From 5d21020614f2d4bcc2f2cde149ab161a0e06a73c Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Tue, 6 Sep 2022 13:29:37 -0700 Subject: [PATCH] Render group story sending state on stories list --- Signal.xcodeproj/project.pbxproj | 8 ++-- .../Context View/StoryItemMediaView.swift | 6 +-- .../Stories/MyStoriesViewController.swift | 36 +--------------- .../Stories/StoriesViewController.swift | 16 ++++++- .../HomeView/Stories/StoryCell.swift | 42 +++++++++++++++---- ...{StoryAuthorUtil.swift => StoryUtil.swift} | 37 +++++++++++++++- .../HomeView/Stories/StoryViewModel.swift | 6 ++- 7 files changed, 97 insertions(+), 54 deletions(-) rename Signal/src/ViewControllers/HomeView/Stories/{StoryAuthorUtil.swift => StoryUtil.swift} (68%) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index fabc9a3b04..62f6252343 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -781,7 +781,7 @@ 669E8FED28B4177900043D28 /* OWSSignalServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FEC28B4177800043D28 /* OWSSignalServiceMock.swift */; }; 669E8FEF28B417D500043D28 /* OWSSignalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FEE28B417D500043D28 /* OWSSignalService.swift */; }; 669E8FF028B4185300043D28 /* OWSURLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FE528B4149200043D28 /* OWSURLSessionMock.swift */; }; - 669E8FF128B41A8500043D28 /* StoryAuthorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668AB0CB28AD610600B31984 /* StoryAuthorUtil.swift */; }; + 669E8FF128B41A8500043D28 /* StoryUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668AB0CB28AD610600B31984 /* StoryUtil.swift */; }; 669E900028B42B7A00043D28 /* SystemStoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FFF28B42B7A00043D28 /* SystemStoryManager.swift */; }; 669E900528B42CCF00043D28 /* SystemStoryManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E900428B42CCF00043D28 /* SystemStoryManagerTest.swift */; }; 669E900728B43F5B00043D28 /* SystemStoryManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E900628B43F5B00043D28 /* SystemStoryManagerProtocol.swift */; }; @@ -3030,7 +3030,7 @@ 661396AE28BE881E00E0C4DF /* ChainedPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedPromiseTest.swift; sourceTree = ""; }; 661602A328BEB94400C1932D /* StoryListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListDataSource.swift; sourceTree = ""; }; 6657FDE7B91C2845BB3BEAB5 /* Pods_SignalShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 668AB0CB28AD610600B31984 /* StoryAuthorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAuthorUtil.swift; sourceTree = ""; }; + 668AB0CB28AD610600B31984 /* StoryUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryUtil.swift; sourceTree = ""; }; 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = ""; }; 668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = ""; }; 668FE09E28B947ED008B9071 /* StoryHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryHidingManager.swift; sourceTree = ""; }; @@ -6105,10 +6105,10 @@ 886292112835606D00AA0C3B /* MyStoryCell.swift */, 8862920F28355B8000AA0C3B /* MyStoryViewModel.swift */, 8852572827DD366D0032073C /* StoriesViewController.swift */, - 668AB0CB28AD610600B31984 /* StoryAuthorUtil.swift */, 884DB94627DD754700C6A309 /* StoryCell.swift */, 661602A328BEB94400C1932D /* StoryListDataSource.swift */, 88B2234B284FABE600A25048 /* StoryThumbnailView.swift */, + 668AB0CB28AD610600B31984 /* StoryUtil.swift */, 884DB94427DD70F700C6A309 /* StoryViewModel.swift */, ); path = Stories; @@ -10374,7 +10374,6 @@ 34B3F87B1E8DF1700035BE1A /* SplashViewController.swift in Sources */, 349767E425B8744700ECE1B0 /* StickerPackViewController.swift in Sources */, 8852572927DD366D0032073C /* StoriesViewController.swift in Sources */, - 669E8FF128B41A8500043D28 /* StoryAuthorUtil.swift in Sources */, 884DB94727DD754700C6A309 /* StoryCell.swift in Sources */, 884DB95027DE67BB00C6A309 /* StoryContextViewController.swift in Sources */, 88423A52280A171E007D2918 /* StoryDirectReplySheet.swift in Sources */, @@ -10398,6 +10397,7 @@ 88863A54280CAEAA00977F69 /* StoryReplySheetAnimator.swift in Sources */, 88863A4E280CAE0800977F69 /* StorySlideAnimator.swift in Sources */, 88B2234C284FABE600A25048 /* StoryThumbnailView.swift in Sources */, + 669E8FF128B41A8500043D28 /* StoryUtil.swift in Sources */, 884DB94527DD70F700C6A309 /* StoryViewModel.swift in Sources */, 88B00D4D28A3346100BC9CA0 /* StoryViewsViewController.swift in Sources */, 88863A50280CAE4400977F69 /* StoryZoomAnimator.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift index 4e30b9a6f6..94c2286021 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryItemMediaView.swift @@ -274,7 +274,7 @@ class StoryItemMediaView: UIView { ) authorAvatarView.update(transaction) { config in - config.dataSource = try? StoryAuthorUtil.authorAvatarDataSource( + config.dataSource = try? StoryUtil.authorAvatarDataSource( for: item.message, transaction: transaction ) @@ -283,7 +283,7 @@ class StoryItemMediaView: UIView { switch item.message.context { case .groupId: guard - let groupAvatarDataSource = try? StoryAuthorUtil.contextAvatarDataSource( + let groupAvatarDataSource = try? StoryUtil.contextAvatarDataSource( for: item.message, transaction: transaction ) @@ -323,7 +323,7 @@ class StoryItemMediaView: UIView { let label = UILabel() label.textColor = Theme.darkThemePrimaryColor label.font = UIFont.ows_dynamicTypeSubheadline.ows_semibold - label.text = StoryAuthorUtil.authorDisplayName( + label.text = StoryUtil.authorDisplayName( for: item.message, contactsManager: contactsManager, useFullNameForLocalAddress: false, diff --git a/Signal/src/ViewControllers/HomeView/Stories/MyStoriesViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/MyStoriesViewController.swift index 29c7bd2768..71513eeff0 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/MyStoriesViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/MyStoriesViewController.swift @@ -129,7 +129,7 @@ extension MyStoriesViewController: UITableViewDelegate { guard let thread = thread(for: indexPath.section), let item = item(for: indexPath) else { return } if item.message.sendingState == .failed { - return askToResend(for: item) + return StoryUtil.askToResend(item.message, in: item.thread, from: self) } let vc = StoryPageViewController( @@ -141,40 +141,6 @@ extension MyStoriesViewController: UITableViewDelegate { vc.contextDataSource = self present(vc, animated: true) } - - private func askToResend(for item: OutgoingStoryItem) { - let actionSheet = ActionSheetController(message: NSLocalizedString( - "STORY_RESEND_MESSAGE_ACTION_SHEET", - comment: "Title for the dialog asking user if they wish to resend a failed story message." - )) - actionSheet.addAction(OWSActionSheets.cancelAction) - actionSheet.addAction(.init(title: CommonStrings.deleteButton, style: .destructive, handler: { _ in - Self.databaseStorage.write { transaction in - item.message.remotelyDelete(for: item.thread, transaction: transaction) - } - })) - actionSheet.addAction(.init(title: CommonStrings.sendMessage, handler: { [weak self] _ in - self?.resend(item: item) - })) - presentActionSheet(actionSheet, animated: true) - } - - private func resend(item: OutgoingStoryItem) { - guard case .outgoing(let recipientStates) = item.message.manifest else { return } - let stoppedBySafetyNumberChange = SafetyNumberConfirmationSheet.presentIfNecessary( - addresses: recipientStates.keys.map { .init(uuid: $0) }, - confirmationText: SafetyNumberStrings.confirmSendButton - ) { [weak self] confirmedSafetyNumberChange in - guard confirmedSafetyNumberChange else { return } - self?.resend(item: item) - } - - guard !stoppedBySafetyNumberChange else { return } - - Self.databaseStorage.write { transaction in - item.message.resendMessageToFailedRecipients(transaction: transaction) - } - } } extension MyStoriesViewController: UITableViewDataSource { diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift index 43a57beada..bf16d3d46c 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift @@ -80,7 +80,7 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate { case .visibleStories, .hiddenStories: guard let cell = self.tableView.cellForRow(at: indexPath) as? StoryCell else { continue } guard let model = self.model(for: indexPath) else { continue } - cell.configureTimestamp(with: model) + cell.configureSubtitle(with: model) case .none: owsFailDebug("Unexpected story type") } @@ -243,6 +243,20 @@ extension StoriesViewController: UITableViewDelegate { return } + // Navigate to "My Stories" rather than the viewer if the message is failed + if model.latestMessageSendingState == .failed { + guard let latestMessage = model.messages.last else { + owsFailDebug("Missing message for failed send") + return + } + guard let latestMessageThread = databaseStorage.read(block: { latestMessage.context.thread(transaction: $0) }) else { + owsFailDebug("Missing thread for failed send") + return + } + StoryUtil.askToResend(latestMessage, in: latestMessageThread, from: self) + return + } + // If we tap on a story with unviewed stories, we only want the viewer // to page through unviewed contexts. let filterViewed = model.hasUnviewedMessages diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift index fdad286b0b..024a152c4e 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryCell.swift @@ -12,12 +12,15 @@ class StoryCell: UITableViewCell { let nameLabel = UILabel() let nameIconView = UIImageView() - let timestampLabel = UILabel() + let subtitleLabel = UILabel() let avatarView = ConversationAvatarView(sizeClass: .fiftySix, localUserDisplayMode: .asUser, useAutolayout: true) let attachmentThumbnail = UIView() let replyImageView = UIImageView() + let failedIconView = UIImageView() + let contentHStackView = UIStackView() + let subtitleStack = UIStackView() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -32,7 +35,16 @@ class StoryCell: UITableViewCell { nameStack.alignment = .center nameStack.spacing = 3 - let vStack = UIStackView(arrangedSubviews: [nameStack, timestampLabel, replyImageView]) + failedIconView.autoSetDimension(.width, toSize: 16) + failedIconView.contentMode = .scaleAspectFit + failedIconView.tintColor = .ows_accentRed + + subtitleStack.addArrangedSubviews([failedIconView, subtitleLabel]) + subtitleStack.axis = .horizontal + subtitleStack.alignment = .center + subtitleStack.spacing = 6 + + let vStack = UIStackView(arrangedSubviews: [nameStack, subtitleStack, replyImageView]) vStack.axis = .vertical vStack.alignment = .leading @@ -54,7 +66,7 @@ class StoryCell: UITableViewCell { } func configure(with model: StoryViewModel) { - configureTimestamp(with: model) + configureSubtitle(with: model) switch model.context { case .authorUuid: @@ -94,10 +106,24 @@ class StoryCell: UITableViewCell { contentView.alpha = model.isHidden ? 0.27 : 1 } - func configureTimestamp(with model: StoryViewModel) { - timestampLabel.isHidden = model.isSystemStory - timestampLabel.font = .ows_dynamicTypeSubheadline - timestampLabel.textColor = Theme.secondaryTextAndIconColor - timestampLabel.text = DateUtil.formatTimestampRelatively(model.latestMessageTimestamp) + func configureSubtitle(with model: StoryViewModel) { + subtitleStack.isHidden = model.isSystemStory + subtitleLabel.font = .ows_dynamicTypeSubheadline + subtitleLabel.textColor = Theme.secondaryTextAndIconColor + + switch model.latestMessageSendingState { + case .sent: + subtitleLabel.text = DateUtil.formatTimestampRelatively(model.latestMessageTimestamp) + failedIconView.isHiddenInStackView = true + case .sending, .pending: + subtitleLabel.text = NSLocalizedString("STORY_SENDING", comment: "Text indicating that the story is currently sending") + failedIconView.isHiddenInStackView = true + case .failed: + subtitleLabel.text = NSLocalizedString("STORY_SEND_FAILED", comment: "Text indicating that the story send has failed") + failedIconView.image = Theme.iconImage(.error16) + failedIconView.isHiddenInStackView = false + case .sent_OBSOLETE, .delivered_OBSOLETE: + owsFailDebug("Unexpected legacy sending state") + } } } diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryAuthorUtil.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryUtil.swift similarity index 68% rename from Signal/src/ViewControllers/HomeView/Stories/StoryAuthorUtil.swift rename to Signal/src/ViewControllers/HomeView/Stories/StoryUtil.swift index 6354db6939..cbfcbefe38 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryAuthorUtil.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryUtil.swift @@ -3,9 +3,10 @@ // import Foundation +import SignalServiceKit /// Container for util methods related to story authors. -public enum StoryAuthorUtil { +public enum StoryUtil: Dependencies { static func authorDisplayName( for storyMessage: StoryMessage, @@ -88,4 +89,38 @@ public enum StoryAuthorUtil { badge: nil ) } + + static func askToResend(_ message: StoryMessage, in thread: TSThread, from vc: UIViewController) { + let actionSheet = ActionSheetController(message: NSLocalizedString( + "STORY_RESEND_MESSAGE_ACTION_SHEET", + comment: "Title for the dialog asking user if they wish to resend a failed story message." + )) + actionSheet.addAction(OWSActionSheets.cancelAction) + actionSheet.addAction(.init(title: CommonStrings.deleteButton, style: .destructive, handler: { _ in + Self.databaseStorage.write { transaction in + message.remotelyDelete(for: thread, transaction: transaction) + } + })) + actionSheet.addAction(.init(title: CommonStrings.sendMessage, handler: { _ in + resend(message) + })) + vc.presentActionSheet(actionSheet, animated: true) + } + + private static func resend(_ message: StoryMessage) { + guard case .outgoing(let recipientStates) = message.manifest else { return } + let stoppedBySafetyNumberChange = SafetyNumberConfirmationSheet.presentIfNecessary( + addresses: recipientStates.keys.map { .init(uuid: $0) }, + confirmationText: SafetyNumberStrings.confirmSendButton + ) { confirmedSafetyNumberChange in + guard confirmedSafetyNumberChange else { return } + resend(message) + } + + guard !stoppedBySafetyNumberChange else { return } + + Self.databaseStorage.write { transaction in + message.resendMessageToFailedRecipients(transaction: transaction) + } + } } diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift index f12ce2708a..661eff20e5 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift @@ -20,6 +20,7 @@ struct StoryViewModel: Dependencies { let latestMessageName: String let latestMessageTimestamp: UInt64 let latestMessageViewedTimestamp: UInt64? + let latestMessageSendingState: TSOutgoingMessageState let threadUniqueId: String? @@ -45,15 +46,16 @@ struct StoryViewModel: Dependencies { self.context = latestMessage.context self.hasReplies = InteractionFinder.hasReplies(for: sortedFilteredMessages, transaction: transaction) - latestMessageName = StoryAuthorUtil.authorDisplayName( + latestMessageName = StoryUtil.authorDisplayName( for: latestMessage, contactsManager: Self.contactsManager, transaction: transaction ) - latestMessageAvatarDataSource = try StoryAuthorUtil.contextAvatarDataSource(for: latestMessage, transaction: transaction) + latestMessageAvatarDataSource = try StoryUtil.contextAvatarDataSource(for: latestMessage, transaction: transaction) latestMessageAttachment = .from(latestMessage.attachment, transaction: transaction) latestMessageTimestamp = latestMessage.timestamp latestMessageViewedTimestamp = latestMessage.localUserViewedTimestamp + latestMessageSendingState = latestMessage.sendingState let threadUniqueId = context.threadUniqueId(transaction: transaction) self.threadUniqueId = threadUniqueId