Commit 2e779ee3 authored by Andreas Traczyk's avatar Andreas Traczyk Committed by Kateryna Kostiuk

avatars: abstract contact avatar UIView generation

- Avatar generation is rampant throughout the iOS client, this
  patch subclasses UIView and initializes an avatar generated
  from the profile image and username of the contact. An extension
  to the String class is also provided to check if it is a valid
  SHA-1(ring Id).

Change-Id: I36f8281caa5856f3913566eba5e078aff8cf65e9
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent 8b84f945
......@@ -251,6 +251,7 @@
621231F91F880EDF009B86F0 /* UILabel+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621231F81F880EDF009B86F0 /* UILabel+Ring.swift */; };
621231FB1F8D6FEE009B86F0 /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621231FA1F8D6FEE009B86F0 /* MessageCell.swift */; };
623660AA20092081002598C1 /* src in Resources */ = {isa = PBXBuildFile; fileRef = 623660A920092081002598C1 /* src */; };
627F11F120348FBF006560B5 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F11F020348FBF006560B5 /* AvatarView.swift */; };
62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */; };
62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */; };
62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */; };
......@@ -542,6 +543,7 @@
621231F81F880EDF009B86F0 /* UILabel+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Ring.swift"; sourceTree = "<group>"; };
621231FA1F8D6FEE009B86F0 /* MessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = "<group>"; };
623660A920092081002598C1 /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = ../../daemon/src; sourceTree = "<group>"; };
627F11F020348FBF006560B5 /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = "<group>"; };
62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PresenceAdapter.h; sourceTree = "<group>"; };
62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceAdapterDelegate.swift; sourceTree = "<group>"; };
62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PresenceAdapter.mm; sourceTree = "<group>"; };
......@@ -1016,12 +1018,11 @@
0E36979B20322D4D009A68CA /* BlockList */ = {
isa = PBXGroup;
children = (
6257022620377D7000D38941 /* Cells */,
0E36979720322D46009A68CA /* BlockListViewController.swift */,
0E36979C20322D75009A68CA /* BlockListViewModel.swift */,
0E36979E20322E6F009A68CA /* BlockListViewController.storyboard */,
0E3697A0203235EA009A68CA /* BannedContactItem.swift */,
0E3697A6203243D3009A68CA /* BannedContactCell.swift */,
0E3697A7203243D3009A68CA /* BannedContactCell.xib */,
);
name = BlockList;
sourceTree = "<group>";
......@@ -1029,6 +1030,7 @@
0E44B62D202B9DC40060F71B /* Helpers */ = {
isa = PBXGroup;
children = (
627F11F020348FBF006560B5 /* AvatarView.swift */,
0E44B62E202B9DE40060F71B /* LocalNotificationsHelper.swift */,
);
path = Helpers;
......@@ -1402,6 +1404,15 @@
name = daemon;
sourceTree = "<group>";
};
6257022620377D7000D38941 /* Cells */ = {
isa = PBXGroup;
children = (
0E3697A7203243D3009A68CA /* BannedContactCell.xib */,
0E3697A6203243D3009A68CA /* BannedContactCell.swift */,
);
name = Cells;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
......@@ -1821,6 +1832,7 @@
1A5DC0321F3566140075E8EF /* ConversationSection.swift in Sources */,
1A2D18C41F29180700B2C785 /* ConfigKeyModel.swift in Sources */,
62AF6862201A66CF003AA9E8 /* AudioAdapter.mm in Sources */,
627F11F120348FBF006560B5 /* AvatarView.swift in Sources */,
1A20417A1F1E547F00C08435 /* Stateable.swift in Sources */,
1A2D18F51F292D7200B2C785 /* MessageCellReceived.swift in Sources */,
56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */,
......
......@@ -33,6 +33,18 @@ extension String {
}
}
func isSHA1() -> Bool {
do {
let sha1Regex = try NSRegularExpression(pattern: "[0-9a-f]{40}", options: .caseInsensitive)
if !sha1Regex.matches(in: self, range: NSRange(location: 0, length: self.count)).isEmpty {
return true
}
} catch {
print("Bad regex")
}
return false
}
func toMD5HexString() -> String {
let messageData = self.data(using: .utf8)!
var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
......
......@@ -22,40 +22,19 @@ import Foundation
import UIKit
extension UIColor {
static let ringMain = UIColor(red: 58.0/255.0,
green: 192.0/255.0,
blue: 210.0/255.0,
alpha: 1.0)
static let ringSecondary = UIColor(red: 0.0/255.0,
green: 76.0/255.0,
blue: 96.0/255.0,
alpha: 1.0)
static let ringMsgCellSent = UIColor(red: 58.0/255.0,
green: 192.0/255.0,
blue: 210.0/255.0,
alpha: 1.0)
static let ringMsgCellSentText = UIColor(red: 255.0/255.0,
green: 255.0/255.0,
blue: 255.0/255.0,
alpha: 1.0)
static let ringMsgCellReceived = UIColor(red: 235.0/255.0,
green: 239.0/255.0,
blue: 239.0/255.0,
alpha: 1.0)
static let ringMsgCellReceivedText = UIColor(red: 48.0/255.0,
green: 48.0/255.0,
blue: 48.0/255.0,
alpha: 1.0)
static let ringMsgCellTimeText = UIColor(red: 128.0/255.0,
green: 128.0/255.0,
blue: 128.0/255.0,
alpha: 1.0)
convenience init(red: Int, green: Int, blue: Int, alpha: CGFloat) {
let red_ = CGFloat(red) / 255.0
let green_ = CGFloat(green) / 255.0
let blue_ = CGFloat(blue) / 255.0
self.init(red: red_, green: green_, blue: blue_, alpha: alpha)
}
static let ringMain = UIColor(red: 58, green: 192, blue: 210, alpha: 1.0)
static let ringSecondary = UIColor(red: 0, green: 76, blue: 96, alpha: 1.0)
static let ringMsgCellSent = UIColor(red: 58, green: 192, blue: 210, alpha: 1.0)
static let ringMsgCellSentText = UIColor(red: 255, green: 255, blue: 255, alpha: 1.0)
static let ringMsgCellReceived = UIColor(red: 235, green: 239, blue: 239, alpha: 1.0)
static let ringMsgCellReceivedText = UIColor(red: 48, green: 48, blue: 48, alpha: 1.0)
static let ringMsgCellTimeText = UIColor(red: 128, green: 128, blue: 128, alpha: 1.0)
static let ringUITableViewCellSelection = UIColor(red: 209, green: 210, blue: 210, alpha: 1.0)
}
......@@ -2,6 +2,7 @@
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -24,24 +25,25 @@ import RxSwift
class ContactRequestCell: UITableViewCell, NibReusable {
@IBOutlet weak var fallbackAvatar: UILabel!
@IBOutlet weak var profileImageView: UIImageView!
@IBOutlet weak var fallbackAvatarImage: UIImageView!
@IBOutlet weak var avatarView: UIView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var acceptButton: UIButton!
@IBOutlet weak var discardButton: UIButton!
@IBOutlet weak var banButton: UIButton!
override func setSelected(_ selected: Bool, animated: Bool) {
let fallbackAvatarBGColor = self.fallbackAvatar.backgroundColor
super.setSelected(selected, animated: animated)
self.fallbackAvatar.backgroundColor = fallbackAvatarBGColor
self.backgroundColor = UIColor.ringUITableViewCellSelection
UIView.animate(withDuration: 0.35, animations: {
self.backgroundColor = UIColor.ringUITableViewCellSelection.lighten(byPercentage: 5.0)
})
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
let fallbackAvatarBGColor = self.fallbackAvatar.backgroundColor
super.setSelected(highlighted, animated: animated)
self.fallbackAvatar.backgroundColor = fallbackAvatarBGColor
if highlighted {
self.backgroundColor = UIColor.ringUITableViewCellSelection
} else {
self.backgroundColor = UIColor.clear
}
}
var disposeBag = DisposeBag()
......@@ -51,80 +53,29 @@ class ContactRequestCell: UITableViewCell, NibReusable {
}
func configureFromItem(_ item: ContactRequestItem) {
item.userName
.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: self.nameLabel.rx.text)
.disposed(by: self.disposeBag)
// Avatar placeholder initial
self.fallbackAvatar.text = nil
self.fallbackAvatarImage.isHidden = true
let name = item.userName.value
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
self.fallbackAvatar.isHidden = false
self.fallbackAvatar.backgroundColor = avatarColors[Int(index)]
if item.contactRequest.ringId != name {
self.fallbackAvatar.text = name.prefixString().capitalized
} else {
self.fallbackAvatarImage.isHidden = false
// avatar
Observable<(Data?, String)>.combineLatest(item.profileImageData.asObservable(),
item.userName.asObservable()) { profileImage, username in
return (profileImage, username)
}
}
item.userName.asObservable()
.observeOn(MainScheduler.instance)
.filter({ [weak item] userName in
return userName != item?.contactRequest.ringId
.startWith((item.profileImageData.value, item.userName.value))
.subscribe({ [weak self] profileData -> Void in
self?.avatarView.subviews.forEach({ $0.removeFromSuperview() })
self?.avatarView.addSubview(AvatarView(profileImageData: profileData.element?.0,
username: (profileData.element?.1)!,
size: 40))
return
})
.map { value in value.prefixString().capitalized }
.bind(to: self.fallbackAvatar.rx.text)
.disposed(by: self.disposeBag)
item.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { [weak item] userName in userName != item?.contactRequest.ringId }
.bind(to: self.fallbackAvatarImage.rx.isHidden)
.disposed(by: self.disposeBag)
// UIColor that observes "best Id" prefix
item.userName.asObservable()
// name
item.userName
.asObservable()
.observeOn(MainScheduler.instance)
.map { name in
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
return avatarColors[Int(index)]
}
return defaultAvatarColor
}
.subscribe(onNext: { backgroundColor in
self.fallbackAvatar.backgroundColor = backgroundColor
})
.bind(to: self.nameLabel.rx.text)
.disposed(by: self.disposeBag)
// Set image if any
if let imageData = item.profileImageData.value {
if let image = UIImage(data: imageData) {
self.profileImageView.image = image
self.fallbackAvatar.isHidden = true
}
} else {
self.fallbackAvatar.isHidden = false
self.profileImageView.image = nil
}
item.profileImageData.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
if let imageData = data, let image = UIImage(data: imageData) {
self.fallbackAvatar.isHidden = true
self.profileImageView.image = image
} else {
self.fallbackAvatar.isHidden = false
self.profileImageView.image = nil
}
}).disposed(by: self.disposeBag)
self.selectionStyle = .none
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13174"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
......@@ -18,40 +18,14 @@
<rect key="frame" x="0.0" y="0.0" width="470" height="71.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Wjc-Nn-INi" userLabel="Fallback Avatar">
<rect key="frame" x="16" y="16" width="40" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="fallback_avatar" translatesAutoresizingMaskIntoConstraints="NO" id="B6v-9R-dRD" userLabel="Fallback image">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9NG-xW-Y5g" userLabel="Avatar View">
<rect key="frame" x="16" y="16" width="40" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="3ok-c3-kW9"/>
<constraint firstAttribute="width" constant="40" id="PkF-FM-Zff"/>
<constraint firstAttribute="height" constant="40" id="2fD-fw-BFv"/>
<constraint firstAttribute="width" constant="40" id="7gu-BP-2Ld"/>
</constraints>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xS9-Kd-lrg">
<rect key="frame" x="16" y="16" width="40" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="1np-vL-9Yu"/>
<constraint firstAttribute="height" constant="40" id="Ghu-V0-iZL"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="feR-9F-sZM">
<rect key="frame" x="244" y="33.5" width="70" height="30"/>
<constraints>
......@@ -127,36 +101,29 @@
</subviews>
<constraints>
<constraint firstItem="Pni-bm-rkr" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="4" id="0jS-6Q-w8N"/>
<constraint firstItem="xS9-Kd-lrg" firstAttribute="centerY" secondItem="YU4-Oq-lYT" secondAttribute="centerY" id="DPx-jS-V0h"/>
<constraint firstItem="B6v-9R-dRD" firstAttribute="leading" secondItem="xS9-Kd-lrg" secondAttribute="leading" id="Dcd-fF-cHv"/>
<constraint firstItem="feR-9F-sZM" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="4" id="Gba-vQ-Ebp"/>
<constraint firstAttribute="bottom" secondItem="Pni-bm-rkr" secondAttribute="bottom" constant="8" id="KNE-9A-qWk"/>
<constraint firstItem="9NG-xW-Y5g" firstAttribute="centerY" secondItem="YU4-Oq-lYT" secondAttribute="centerY" id="Ofv-lu-x2h"/>
<constraint firstItem="fWB-HR-tae" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="4" id="QMX-zx-zpZ"/>
<constraint firstItem="Pni-bm-rkr" firstAttribute="leading" secondItem="fWB-HR-tae" secondAttribute="trailing" constant="4" id="XcH-3n-PeT"/>
<constraint firstAttribute="bottom" secondItem="fWB-HR-tae" secondAttribute="bottom" constant="8" id="Z44-tV-yAn"/>
<constraint firstAttribute="bottom" secondItem="feR-9F-sZM" secondAttribute="bottom" constant="8" id="hAP-C0-nE5"/>
<constraint firstItem="Dla-OF-biH" firstAttribute="top" secondItem="YU4-Oq-lYT" secondAttribute="top" constant="8" id="hqf-Iv-xvb"/>
<constraint firstItem="fWB-HR-tae" firstAttribute="leading" secondItem="feR-9F-sZM" secondAttribute="trailing" constant="4" id="lfX-2s-AsZ"/>
<constraint firstItem="xS9-Kd-lrg" firstAttribute="leading" secondItem="YU4-Oq-lYT" secondAttribute="leading" constant="16" id="ogz-Qb-1Pz"/>
<constraint firstItem="B6v-9R-dRD" firstAttribute="top" secondItem="xS9-Kd-lrg" secondAttribute="top" id="sCh-oV-bKL"/>
<constraint firstAttribute="trailing" secondItem="Pni-bm-rkr" secondAttribute="trailing" constant="8" id="sDu-vC-6gU"/>
<constraint firstItem="Dla-OF-biH" firstAttribute="leading" secondItem="9NG-xW-Y5g" secondAttribute="trailing" constant="8" id="ttD-Zx-Fv7"/>
<constraint firstAttribute="trailing" secondItem="Dla-OF-biH" secondAttribute="trailing" constant="8" id="wFU-JT-uD3"/>
<constraint firstItem="Dla-OF-biH" firstAttribute="leading" secondItem="xS9-Kd-lrg" secondAttribute="trailing" constant="8" id="zIp-sT-nb3"/>
<constraint firstItem="9NG-xW-Y5g" firstAttribute="leading" secondItem="YU4-Oq-lYT" secondAttribute="leading" constant="16" id="zsy-tX-JRZ"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="acceptButton" destination="feR-9F-sZM" id="N3u-Ww-sE5"/>
<outlet property="avatarView" destination="9NG-xW-Y5g" id="aDt-m2-cB5"/>
<outlet property="banButton" destination="Pni-bm-rkr" id="v1p-37-zfP"/>
<outlet property="discardButton" destination="fWB-HR-tae" id="ZMu-NC-hE4"/>
<outlet property="fallbackAvatar" destination="Wjc-Nn-INi" id="vCB-ug-eic"/>
<outlet property="fallbackAvatarImage" destination="B6v-9R-dRD" id="XV9-ww-vFG"/>
<outlet property="nameLabel" destination="Dla-OF-biH" id="tO5-og-I3P"/>
<outlet property="profileImageView" destination="xS9-Kd-lrg" id="Rxt-j1-fem"/>
</connections>
<point key="canvasLocation" x="177" y="-50"/>
</tableViewCell>
</objects>
<resources>
<image name="fallback_avatar" width="82" height="82"/>
</resources>
</document>
......@@ -26,6 +26,7 @@ import ActiveLabel
class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var avatarView: UIView!
@IBOutlet weak var bubble: MessageBubble!
@IBOutlet weak var bubbleBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var bubbleTopConstraint: NSLayoutConstraint!
......@@ -37,9 +38,6 @@ class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var rightDivider: UIView!
@IBOutlet weak var sendingIndicator: UIActivityIndicatorView!
@IBOutlet weak var failedStatusLabel: UILabel!
@IBOutlet weak var profileImage: UIImageView!
@IBOutlet weak var fallbackAvatar: UILabel!
@IBOutlet weak var fallbackAvatarImage: UIImageView!
var disposeBag = DisposeBag()
......@@ -169,10 +167,9 @@ class MessageCell: UITableViewCell, NibReusable {
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:disable cyclomatic_complexity
func configureFromItem(_ conversationViewModel: ConversationViewModel,
_ items: [MessageViewModel]?,
cellForRowAt indexPath: IndexPath) {
_ items: [MessageViewModel]?,
cellForRowAt indexPath: IndexPath) {
guard let item = items?[indexPath.row] else {
return
}
......@@ -198,6 +195,7 @@ class MessageCell: UITableViewCell, NibReusable {
self.bubbleBottomConstraint.constant = 16
}
// sent message status
if item.bubblePosition() == .sent {
item.status.asObservable()
.observeOn(MainScheduler.instance)
......@@ -210,92 +208,23 @@ class MessageCell: UITableViewCell, NibReusable {
.bind(to: self.failedStatusLabel.rx.isHidden)
.disposed(by: self.disposeBag)
} else if item.bubblePosition() == .received {
// avatar
guard let fallbackAvatar = self.fallbackAvatar else {
return
}
self.fallbackAvatar.isHidden = true
self.profileImage?.isHidden = true
if item.sequencing == .lastOfSequence || item.sequencing == .singleMessage {
self.profileImage?.isHidden = false
// Set placeholder avatar
fallbackAvatar.text = nil
self.fallbackAvatarImage.isHidden = true
let name = conversationViewModel.userName.value
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
fallbackAvatar.isHidden = false
fallbackAvatar.backgroundColor = avatarColors[Int(index)]
if conversationViewModel.conversation.value.recipientRingId != name {
self.fallbackAvatar.text = name.prefixString().capitalized
} else {
self.fallbackAvatarImage.isHidden = true
}
}
// Avatar placeholder color
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { name in
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
return avatarColors[Int(index)]
}
return defaultAvatarColor
}.subscribe(onNext: { backgroundColor in
self.fallbackAvatar.backgroundColor = backgroundColor
})
.disposed(by: self.disposeBag)
// Avatar placeholder initial
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.filter({ userName in
return userName != conversationViewModel.conversation.value.recipientRingId
})
.map { value in
value.prefixString().capitalized
}
.bind(to: self.fallbackAvatar.rx.text)
.disposed(by: self.disposeBag)
// If only the ringId is known, use fallback avatar image
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { userName in
userName != conversationViewModel.conversation.value.recipientRingId
}
.bind(to: self.fallbackAvatarImage.rx.isHidden)
.disposed(by: self.disposeBag)
// Set image if any
if let imageData = conversationViewModel.profileImageData.value {
if let image = UIImage(data: imageData) {
self.profileImage.image = image
self.fallbackAvatar.isHidden = true
}
} else {
self.fallbackAvatar.isHidden = false
self.profileImage.image = nil
// received message avatar
Observable<(Data?, String)>.combineLatest(conversationViewModel.profileImageData.asObservable(),
conversationViewModel.userName.asObservable()) { profileImage, username in
return (profileImage, username)
}
.observeOn(MainScheduler.instance)
.startWith((conversationViewModel.profileImageData.value, conversationViewModel.userName.value))
.subscribe({ [weak self] profileData -> Void in
self?.avatarView.subviews.forEach({ $0.removeFromSuperview() })
self?.avatarView.addSubview(AvatarView(profileImageData: profileData.element?.0,
username: (profileData.element?.1)!,
size: 32))
self?.avatarView.isHidden = !(item.sequencing == .lastOfSequence || item.sequencing == .singleMessage)
return
})
.disposed(by: self.disposeBag)
conversationViewModel.profileImageData.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
if let imageData = data, let image = UIImage(data: imageData) {
self.profileImage.image = image
self.fallbackAvatar.isHidden = true
} else {
self.fallbackAvatar.isHidden = false
self.profileImage.image = nil
}
}).disposed(by: self.disposeBag)
}
}
}
// swiftlint:enable cyclomatic_complexity
}
......@@ -19,42 +19,14 @@
<rect key="frame" x="0.0" y="0.0" width="510" height="46.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="F9a-6w-Efg" userLabel="Fallback Avatar">
<rect key="frame" x="16" y="4" width="32" height="32"/>
<color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nc6-du-YKs" userLabel="Avatar View">
<rect key="frame" x="16" y="4.5" width="32" height="32"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="32" id="C5R-RV-ZRw"/>
<constraint firstAttribute="width" constant="32" id="jnE-tr-sYI"/>
<constraint firstAttribute="width" constant="32" id="7ec-U6-IXo"/>
<constraint firstAttribute="height" constant="32" id="k8i-qN-bbk"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<integer key="value" value="16"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="fallback_avatar" translatesAutoresizingMaskIntoConstraints="NO" id="tOS-Tt-QFy" userLabel="Fallback image">
<rect key="frame" x="16" y="4" width="32" height="32"/>
<constraints>
<constraint firstAttribute="height" constant="32" id="1UM-p9-Mmj"/>
<constraint firstAttribute="width" constant="32" id="BIh-38-mEs"/>
</constraints>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="skH-sJ-Ip9" userLabel="Profile Image">
<rect key="frame" x="16" y="4" width="32" height="32"/>