diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index c696d30cb..34486c858 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -113,7 +113,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat let result = UIView() result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity) let size = ConversationVC.unreadCountViewSize - result.set(.width, to: size) + result.set(.width, greaterThanOrEqualTo: size) result.set(.height, to: size) result.layer.masksToBounds = true result.layer.cornerRadius = size / 2 @@ -193,7 +193,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat // Unread count view view.addSubview(unreadCountView) unreadCountView.addSubview(unreadCountLabel) - unreadCountLabel.pin(to: unreadCountView) + unreadCountLabel.pin(.top, to: .top, of: unreadCountView) + unreadCountLabel.pin(.bottom, to: .bottom, of: unreadCountView) + unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4) + unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4) unreadCountView.centerYAnchor.constraint(equalTo: scrollButton.topAnchor).isActive = true unreadCountView.center(.horizontal, in: scrollButton) updateUnreadCountView() @@ -473,6 +476,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat func scrollToBottom(isAnimated: Bool) { guard !isUserScrolling else { return } + if let interactionID = viewItems.last?.interaction.uniqueId { + self.scrollToInteraction(with: interactionID, position: .top, isAnimated: isAnimated) + return + } // Ensure the view is fully up to date before we try to scroll to the bottom, since // we use the table view's bounds to determine where the bottom is. view.layoutIfNeeded() @@ -503,8 +510,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat unreadViewItems.remove(at: index) } let unreadCount = unreadViewItems.count - unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+" - let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8 + unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+" + let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8 unreadCountLabel.font = .boldSystemFont(ofSize: fontSize) unreadCountView.isHidden = (unreadCount == 0) } diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index a60beeab4..4ebdd168a 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -567,13 +567,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) TSAttachment *_Nullable linkPreviewAttachment = [TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction]; if (!linkPreviewAttachment) { - OWSFailDebug(@"Could not load link preview image attachment."); + OWSLogDebug(@"Could not load link preview image attachment."); } else if (!linkPreviewAttachment.isImage) { - OWSFailDebug(@"Link preview attachment isn't an image."); + OWSLogDebug(@"Link preview attachment isn't an image."); } else if ([linkPreviewAttachment isKindOfClass:[TSAttachmentStream class]]) { TSAttachmentStream *attachmentStream = (TSAttachmentStream *)linkPreviewAttachment; if (!attachmentStream.isValidImage) { - OWSFailDebug(@"Link preview image attachment isn't valid."); + OWSLogDebug(@"Link preview image attachment isn't valid."); } else { self.linkPreviewAttachment = linkPreviewAttachment; } diff --git a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift b/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift index 9be711311..a0c655a5f 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift @@ -56,7 +56,7 @@ final class MediaTextOverlayView : UIView { self.readMoreButton = readMoreButton readMoreButton.setTitle("Read More", for: UIControl.State.normal) readMoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize) - readMoreButton.setTitleColor(.white, for: UIControl.State.normal) + readMoreButton.setTitleColor(self.textColor, for: UIControl.State.normal) readMoreButton.addTarget(self, action: #selector(readMore), for: UIControl.Event.touchUpInside) addSubview(readMoreButton) readMoreButton.pin(.left, to: .left, of: self, withInset: inset) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0a9755acf..1106b2d10 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -131,6 +131,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private var bodyLabelTextColor: UIColor { switch (direction, AppModeManager.shared.currentAppMode) { case (.outgoing, .dark), (.incoming, .light): return .black + case (.outgoing, .light): return Colors.grey default: return .white } } @@ -207,8 +208,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { // MARK: Updating override func update() { guard let viewItem = viewItem, let message = viewItem.interaction as? TSMessage else { return } - let thread = message.thread - let isGroupThread = thread.isGroupThread() + let isGroupThread = viewItem.isGroupThread // Profile picture view profilePictureViewLeftConstraint.constant = isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0 profilePictureViewWidthConstraint.constant = isGroupThread ? VisibleMessageCell.profilePictureSize : 0 @@ -217,8 +217,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { if let senderSessionID = senderSessionID { profilePictureView.update(for: senderSessionID) } - if let thread = thread as? TSGroupThread, thread.isOpenGroup, let senderSessionID = senderSessionID { - if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) { + if let senderSessionID = senderSessionID, message.isOpenGroupMessage { + if let openGroupV2 = Storage.shared.getV2OpenGroup(for: message.uniqueThreadId) { let isUserModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server) moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden } else { @@ -639,8 +639,15 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let maxAspectRatio = 1 / minAspectRatio aspectRatio = aspectRatio.clamp(minAspectRatio, maxAspectRatio) let maxSize = CGSize(width: maxMessageWidth, height: maxMessageWidth) - var width = with(maxSize.height * aspectRatio) { $0 > maxSize.width ? maxSize.width : $0 } - var height = (width > maxSize.width) ? (maxSize.width / aspectRatio) : maxSize.height + var width: CGFloat + var height: CGFloat + if aspectRatio > 1 { + width = maxSize.width + height = width / aspectRatio + } else { + height = maxSize.height + width = height * aspectRatio + } // Don't blow up small images unnecessarily let minSize: CGFloat = 150 let shortSourceDimension = min(size.width, size.height) diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index cc2afeca1..9ab55638a 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -601,6 +601,8 @@ "light_mode_theme" = "Light"; "PIN_BUTTON_TEXT" = "Pin"; "UNPIN_BUTTON_TEXT" = "Unpin"; +"meida_saved" = "Media saved by %@."; +"screenshot_taken" = "%@ took a screenshot."; "SEARCH_SECTION_CONTACTS" = "Contacts and Groups"; "SEARCH_SECTION_MESSAGES" = "Messages"; "SEARCH_SECTION_RECENT" = "Recent"; diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 1036394f4..866513807 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -143,7 +143,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView Storage.shared.write { transaction in OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction) .done(on: DispatchQueue.main) { [weak self] _ in - self?.presentingViewController!.dismiss(animated: true, completion: nil) + self?.presentingViewController?.dismiss(animated: true, completion: nil) let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) } diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 9969fe8fc..2f6a47f2f 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -28,7 +28,7 @@ final class ConversationCell : UITableViewCell { let result = UIView() result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity) let size = ConversationCell.unreadCountViewSize - result.set(.width, to: size) + result.set(.width, greaterThanOrEqualTo: size) result.set(.height, to: size) result.layer.masksToBounds = true result.layer.cornerRadius = size / 2 @@ -150,7 +150,9 @@ final class ConversationCell : UITableViewCell { profilePictureView.size = profilePictureViewSize // Unread count view unreadCountView.addSubview(unreadCountLabel) - unreadCountLabel.pin(to: unreadCountView) + unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView) + unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4) + unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4) // Has mention view hasMentionView.addSubview(hasMentionLabel) hasMentionLabel.pin(to: hasMentionView) @@ -293,8 +295,8 @@ final class ConversationCell : UITableViewCell { isPinnedIcon.isHidden = !threadViewModel.isPinned unreadCountView.isHidden = !threadViewModel.hasUnreadMessages let unreadCount = threadViewModel.unreadCount - unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+" - let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8 + unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+" + let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8 unreadCountLabel.font = .boldSystemFont(ofSize: fontSize) hasMentionView.isHidden = !(threadViewModel.hasUnreadMentions && thread.isGroupThread()) profilePictureView.update(for: thread) diff --git a/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m b/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m index 01769a1aa..e80bf0bdb 100644 --- a/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m @@ -155,31 +155,10 @@ NS_ASSUME_NONNULL_BEGIN if (_read && readTimestamp >= self.expireStartedAt) { return; } - BOOL isTrusted = YES; - TSThread* thread = [self threadWithTransaction:transaction]; - if ([thread isKindOfClass:[TSContactThread class]]) { - TSContactThread* contactThread = (TSContactThread*)thread; - isTrusted = [[LKStorage shared] getContactWithSessionID:[contactThread contactSessionID] using:transaction].isTrusted; - } - - BOOL areAllAttachmentsDownloaded = YES; - if (isTrusted) { - for (NSString *attachmentId in self.attachmentIds) { - TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; - // If the attachment download failed, we can mark this message as read. - // Otherwise, this message will never be marked as read. - if ([attachment isKindOfClass:[TSAttachmentPointer class]] - && ((TSAttachmentPointer *)attachment).state == TSAttachmentPointerStateFailed) { - continue; - } - areAllAttachmentsDownloaded = areAllAttachmentsDownloaded && attachment.isDownloaded; - if (!areAllAttachmentsDownloaded) break; - } - } - - if (!areAllAttachmentsDownloaded) { - return; - } + // We just ignore all attachments download state here and mark all messages as read + // This is a workaround for a situation that some large attachments won't be downloaded + // and just stuck in a downloading state. In that case, the corresponding message won't + // be able to be marked as read. _read = YES; [self saveWithTransaction:transaction]; diff --git a/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift b/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift index df2477e25..18ceef7f5 100644 --- a/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift +++ b/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift @@ -19,10 +19,10 @@ final class DataExtractionNotificationInfoMessage : TSInfoMessage { let sessionID = thread.contactSessionID() let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID switch messageType { - case .screenshotNotification: return "\(displayName) took a screenshot." + case .screenshotNotification: return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName) case .mediaSavedNotification: // TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved - return "Media saved by \(displayName)." + return String(format: NSLocalizedString("meida_saved", comment: ""), displayName) default: preconditionFailure() } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index 00148075a..1142ff4d8 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -44,7 +44,7 @@ public final class ClosedGroupPoller : NSObject { // Might be a race condition that the setUpPolling finishes too soon, // and the timer is not created, if we mark the group as is polling // after setUpPolling. So the poller may not work, thus misses messages. - isPolling[groupPublicKey] = true + internalQueue.sync{ isPolling[groupPublicKey] = true } setUpPolling(for: groupPublicKey) } @@ -55,7 +55,7 @@ public final class ClosedGroupPoller : NSObject { } public func stopPolling(for groupPublicKey: String) { - isPolling[groupPublicKey] = false + internalQueue.sync{ isPolling[groupPublicKey] = false } timers[groupPublicKey]?.invalidate() } diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 35f003175..8fa8dfba3 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -13,6 +13,7 @@ import UIKit @objc(LKColors) public final class Colors : NSObject { + @objc public static var grey: UIColor { UIColor(named: "session_grey")! } @objc public static var accent: UIColor { UIColor(named: "session_accent")! } @objc public static var text: UIColor { UIColor(named: "session_text")! } @objc public static var destructive: UIColor { UIColor(named: "session_destructive")! } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json new file mode 100644 index 000000000..f347d967f --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x30", + "green" : "0x2F", + "red" : "0x31" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SessionUIKit/Utilities/UIView+Constraints.swift b/SessionUIKit/Utilities/UIView+Constraints.swift index 0dcad6829..e62e27940 100644 --- a/SessionUIKit/Utilities/UIView+Constraints.swift +++ b/SessionUIKit/Utilities/UIView+Constraints.swift @@ -95,4 +95,17 @@ public extension UIView { constraint.isActive = true return constraint } + + @discardableResult + func set(_ dimension: Dimension, greaterThanOrEqualTo size: CGFloat) -> NSLayoutConstraint { + translatesAutoresizingMaskIntoConstraints = false + let constraint: NSLayoutConstraint = { + switch dimension { + case .width: return widthAnchor.constraint(greaterThanOrEqualToConstant: size) + case .height: return heightAnchor.constraint(greaterThanOrEqualToConstant: size) + } + }() + constraint.isActive = true + return constraint + } }