Render group story sending state on stories list

This commit is contained in:
Nora Trapp 2022-09-06 13:29:37 -07:00 committed by Nora Trapp
parent bf1575f819
commit 5d21020614
7 changed files with 97 additions and 54 deletions

View File

@ -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 = "<group>"; };
661602A328BEB94400C1932D /* StoryListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListDataSource.swift; sourceTree = "<group>"; };
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 = "<group>"; };
668AB0CB28AD610600B31984 /* StoryUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryUtil.swift; sourceTree = "<group>"; };
668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = "<group>"; };
668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = "<group>"; };
668FE09E28B947ED008B9071 /* StoryHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryHidingManager.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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)
}
}
}

View File

@ -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