Merge pull request #370 from RyanRory/feedback-for-actions

Add feedbacks for actions in message info screen
pull/1061/head
Morgan Pretty 1 week ago committed by GitHub
commit 507bcc6fa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,11 +15,13 @@ extension ContextMenuVC {
struct Action { struct Action {
let icon: UIImage? let icon: UIImage?
let title: String let title: String
let feedback: String?
let expirationInfo: ExpirationInfo? let expirationInfo: ExpirationInfo?
let themeColor: ThemeValue let themeColor: ThemeValue
let actionType: ActionType let actionType: ActionType
let shouldDismissInfoScreen: Bool
let accessibilityLabel: String? let accessibilityLabel: String?
let work: () -> Void let work: ((() -> Void)?) -> Void
enum ActionType { enum ActionType {
case emoji case emoji
@ -33,17 +35,21 @@ extension ContextMenuVC {
init( init(
icon: UIImage? = nil, icon: UIImage? = nil,
title: String = "", title: String = "",
feedback: String? = nil,
expirationInfo: ExpirationInfo? = nil, expirationInfo: ExpirationInfo? = nil,
themeColor: ThemeValue = .textPrimary, themeColor: ThemeValue = .textPrimary,
actionType: ActionType = .generic, actionType: ActionType = .generic,
shouldDismissInfoScreen: Bool = false,
accessibilityLabel: String? = nil, accessibilityLabel: String? = nil,
work: @escaping () -> Void work: @escaping ((() -> Void)?) -> Void
) { ) {
self.icon = icon self.icon = icon
self.title = title self.title = title
self.feedback = feedback
self.expirationInfo = expirationInfo self.expirationInfo = expirationInfo
self.themeColor = themeColor self.themeColor = themeColor
self.actionType = actionType self.actionType = actionType
self.shouldDismissInfoScreen = shouldDismissInfoScreen
self.accessibilityLabel = accessibilityLabel self.accessibilityLabel = accessibilityLabel
self.work = work self.work = work
} }
@ -55,7 +61,7 @@ extension ContextMenuVC {
icon: UIImage(named: "ic_info"), icon: UIImage(named: "ic_info"),
title: "info".localized(), title: "info".localized(),
accessibilityLabel: "Message info" accessibilityLabel: "Message info"
) { delegate?.info(cellViewModel) } ) { _ in delegate?.info(cellViewModel) }
} }
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -66,31 +72,34 @@ extension ContextMenuVC {
"resend".localized() "resend".localized()
), ),
accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message") accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message")
) { delegate?.retry(cellViewModel) } ) { completion in delegate?.retry(cellViewModel, completion: completion) }
} }
static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_reply"), icon: UIImage(named: "ic_reply"),
title: "reply".localized(), title: "reply".localized(),
shouldDismissInfoScreen: true,
accessibilityLabel: "Reply to message" accessibilityLabel: "Reply to message"
) { delegate?.reply(cellViewModel) } ) { completion in delegate?.reply(cellViewModel, completion: completion) }
} }
static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_copy"), icon: UIImage(named: "ic_copy"),
title: "copy".localized(), title: "copy".localized(),
feedback: "copied".localized(),
accessibilityLabel: "Copy text" accessibilityLabel: "Copy text"
) { delegate?.copy(cellViewModel) } ) { completion in delegate?.copy(cellViewModel, completion: completion) }
} }
static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_copy"), icon: UIImage(named: "ic_copy"),
title: "accountIDCopy".localized(), title: "accountIDCopy".localized(),
feedback: "copied".localized(),
accessibilityLabel: "Copy Session ID" accessibilityLabel: "Copy Session ID"
) { delegate?.copySessionID(cellViewModel) } ) { completion in delegate?.copySessionID(cellViewModel, completion: completion) }
} }
static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -102,16 +111,18 @@ extension ContextMenuVC {
expiresInSeconds: cellViewModel.expiresInSeconds expiresInSeconds: cellViewModel.expiresInSeconds
), ),
themeColor: .danger, themeColor: .danger,
shouldDismissInfoScreen: true,
accessibilityLabel: "Delete message" accessibilityLabel: "Delete message"
) { delegate?.delete(cellViewModel) } ) { completion in delegate?.delete(cellViewModel, completion: completion) }
} }
static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_download"), icon: UIImage(named: "ic_download"),
title: "save".localized(), title: "save".localized(),
feedback: "saved".localized(),
accessibilityLabel: "Save attachment" accessibilityLabel: "Save attachment"
) { delegate?.save(cellViewModel) } ) { completion in delegate?.save(cellViewModel, completion: completion) }
} }
static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -120,7 +131,7 @@ extension ContextMenuVC {
title: "banUser".localized(), title: "banUser".localized(),
themeColor: .danger, themeColor: .danger,
accessibilityLabel: "Ban user" accessibilityLabel: "Ban user"
) { delegate?.ban(cellViewModel) } ) { completion in delegate?.ban(cellViewModel, completion: completion) }
} }
static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -128,28 +139,29 @@ extension ContextMenuVC {
icon: UIImage(named: "ic_block"), icon: UIImage(named: "ic_block"),
title: "banDeleteAll".localized(), title: "banDeleteAll".localized(),
themeColor: .danger, themeColor: .danger,
shouldDismissInfoScreen: true,
accessibilityLabel: "Ban user and delete" accessibilityLabel: "Ban user and delete"
) { delegate?.banAndDeleteAllMessages(cellViewModel) } ) { completion in delegate?.banAndDeleteAllMessages(cellViewModel, completion: completion) }
} }
static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action { static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
title: emoji.rawValue, title: emoji.rawValue,
actionType: .emoji actionType: .emoji
) { delegate?.react(cellViewModel, with: emoji) } ) { _ in delegate?.react(cellViewModel, with: emoji) }
} }
static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
actionType: .emojiPlus, actionType: .emojiPlus,
accessibilityLabel: "Add emoji" accessibilityLabel: "Add emoji"
) { delegate?.showFullEmojiKeyboard(cellViewModel) } ) { _ in delegate?.showFullEmojiKeyboard(cellViewModel) }
} }
static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action { static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
actionType: .dismiss actionType: .dismiss
) { delegate?.contextMenuDismissed() } ) { _ in delegate?.contextMenuDismissed() }
} }
} }
@ -295,14 +307,14 @@ extension ContextMenuVC {
protocol ContextMenuActionDelegate { protocol ContextMenuActionDelegate {
func info(_ cellViewModel: MessageViewModel) func info(_ cellViewModel: MessageViewModel)
func retry(_ cellViewModel: MessageViewModel) func retry(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func reply(_ cellViewModel: MessageViewModel) func reply(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func copy(_ cellViewModel: MessageViewModel) func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func copySessionID(_ cellViewModel: MessageViewModel) func copySessionID(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func delete(_ cellViewModel: MessageViewModel) func delete(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func save(_ cellViewModel: MessageViewModel) func save(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func ban(_ cellViewModel: MessageViewModel) func ban(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, completion: (() -> Void)?)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones)
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel)
func contextMenuDismissed() func contextMenuDismissed()

@ -161,7 +161,7 @@ extension ContextMenuVC {
} }
@objc private func handleTap() { @objc private func handleTap() {
action.work() action.work() {}
dismissWithTimerInvalidationIfNeeded() dismissWithTimerInvalidationIfNeeded()
} }

@ -48,7 +48,7 @@ extension ContextMenuVC {
// MARK: - Interaction // MARK: - Interaction
@objc private func handleTap() { @objc private func handleTap() {
action.work() action.work() {}
dismiss() dismiss()
} }
} }
@ -106,7 +106,7 @@ extension ContextMenuVC {
dismiss() dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { [weak self] in
self?.action?.work() self?.action?.work() {}
}) })
} }
} }

@ -415,7 +415,7 @@ final class ContextMenuVC: UIViewController {
}, },
completion: { [weak self] _ in completion: { [weak self] _ in
self?.dismiss() self?.dismiss()
self?.actions.first(where: { $0.actionType == .dismiss })?.work() self?.actions.first(where: { $0.actionType == .dismiss })?.work(){}
} }
) )
} }

@ -1309,7 +1309,7 @@ extension ConversationVC:
} }
func handleReplyButtonTapped(for cellViewModel: MessageViewModel) { func handleReplyButtonTapped(for cellViewModel: MessageViewModel) {
reply(cellViewModel) reply(cellViewModel, completion: nil)
} }
func startThread( func startThread(
@ -1883,7 +1883,7 @@ extension ConversationVC:
} }
} }
func retry(_ cellViewModel: MessageViewModel) { func retry(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
guard cellViewModel.id != MessageViewModel.optimisticUpdateId else { guard cellViewModel.id != MessageViewModel.optimisticUpdateId else {
guard guard
let optimisticMessageId: UUID = cellViewModel.optimisticMessageId, let optimisticMessageId: UUID = cellViewModel.optimisticMessageId,
@ -1895,7 +1895,10 @@ extension ConversationVC:
title: "theError".localized(), title: "theError".localized(),
body: .text("shareExtensionDatabaseError".localized()), body: .text("shareExtensionDatabaseError".localized()),
cancelTitle: "okay".localized(), cancelTitle: "okay".localized(),
cancelStyle: .alert_text cancelStyle: .alert_text,
afterClosed: {
completion?()
}
) )
) )
@ -1905,6 +1908,7 @@ extension ConversationVC:
// Try to send the optimistic message again // Try to send the optimistic message again
sendMessage(optimisticData: optimisticMessageData) sendMessage(optimisticData: optimisticMessageData)
completion?()
return return
} }
@ -1953,9 +1957,11 @@ extension ConversationVC:
using: dependencies using: dependencies
) )
} }
completion?()
} }
func reply(_ cellViewModel: MessageViewModel) { func reply(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending( let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
authorId: cellViewModel.authorId, authorId: cellViewModel.authorId,
@ -1976,9 +1982,10 @@ extension ConversationVC:
isOutgoing: (cellViewModel.variant == .standardOutgoing) isOutgoing: (cellViewModel.variant == .standardOutgoing)
) )
_ = snInputView.becomeFirstResponder() _ = snInputView.becomeFirstResponder()
completion?()
} }
func copy(_ cellViewModel: MessageViewModel) { func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
switch cellViewModel.cellType { switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break case .typingIndicator, .dateHeader, .unreadMarker: break
@ -2006,15 +2013,35 @@ extension ConversationVC:
UIPasteboard.general.setData(data, forPasteboardType: type.identifier) UIPasteboard.general.setData(data, forPasteboardType: type.identifier)
} }
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in
self?.viewModel.showToast(
text: "copied".localized(),
backgroundColor: .toast_background,
inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0)
)
}
completion?()
} }
func copySessionID(_ cellViewModel: MessageViewModel) { func copySessionID(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
guard cellViewModel.variant == .standardIncoming else { return } guard cellViewModel.variant == .standardIncoming else { return }
UIPasteboard.general.string = cellViewModel.authorId UIPasteboard.general.string = cellViewModel.authorId
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in
self?.viewModel.showToast(
text: "copied".localized(),
backgroundColor: .toast_background,
inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0)
)
}
completion?()
} }
func delete(_ cellViewModel: MessageViewModel) { func delete(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
/// Retrieve the deletion actions for the selected message(s) of there are any /// Retrieve the deletion actions for the selected message(s) of there are any
let messagesToDelete: [MessageViewModel] = [cellViewModel] let messagesToDelete: [MessageViewModel] = [cellViewModel]
@ -2098,6 +2125,7 @@ extension ConversationVC:
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} }
completion?()
} }
} }
) )
@ -2115,7 +2143,7 @@ extension ConversationVC:
} }
} }
func save(_ cellViewModel: MessageViewModel) { func save(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
guard cellViewModel.cellType == .mediaMessage else { return } guard cellViewModel.cellType == .mediaMessage else { return }
let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? [])
@ -2155,7 +2183,15 @@ extension ConversationVC:
) )
} }
}, },
completionHandler: { _, _ in } completionHandler: { _, _ in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in
self?.viewModel.showToast(
text: "saved".localized(),
backgroundColor: .toast_background,
inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0)
)
}
}
) )
} }
@ -2166,9 +2202,11 @@ extension ConversationVC:
self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)))
} }
completion?()
} }
func ban(_ cellViewModel: MessageViewModel) { func ban(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
@ -2201,36 +2239,38 @@ extension ConversationVC:
.receive(on: DispatchQueue.main, using: dependencies) .receive(on: DispatchQueue.main, using: dependencies)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { DispatchQueue.main.async { [weak self] in
case .finished: switch result {
DispatchQueue.main.async { [weak self] in case .finished:
self?.viewModel.showToast( self?.viewModel.showToast(
text: "banUserBanned".localized(), text: "banUserBanned".localized(),
backgroundColor: .backgroundSecondary, backgroundColor: .backgroundSecondary,
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} case .failure:
case .failure:
DispatchQueue.main.async { [weak self] in
self?.viewModel.showToast( self?.viewModel.showToast(
text: "banErrorFailed".localized(), text: "banErrorFailed".localized(),
backgroundColor: .backgroundSecondary, backgroundColor: .backgroundSecondary,
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} }
completion?()
} }
} }
) )
self?.becomeFirstResponder() self?.becomeFirstResponder()
}, },
afterClosed: { [weak self] in self?.becomeFirstResponder() } afterClosed: { [weak self] in
completion?()
self?.becomeFirstResponder()
}
) )
) )
self.present(modal, animated: true) self.present(modal, animated: true)
} }
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) { func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
@ -2263,30 +2303,31 @@ extension ConversationVC:
.receive(on: DispatchQueue.main, using: dependencies) .receive(on: DispatchQueue.main, using: dependencies)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { DispatchQueue.main.async { [weak self] in
case .finished: switch result {
DispatchQueue.main.async { [weak self] in case .finished:
self?.viewModel.showToast( self?.viewModel.showToast(
text: "banUserBanned".localized(), text: "banUserBanned".localized(),
backgroundColor: .backgroundSecondary, backgroundColor: .backgroundSecondary,
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} case .failure:
case .failure:
DispatchQueue.main.async { [weak self] in
self?.viewModel.showToast( self?.viewModel.showToast(
text: "banErrorFailed".localized(), text: "banErrorFailed".localized(),
backgroundColor: .backgroundSecondary, backgroundColor: .backgroundSecondary,
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
) )
} }
completion?()
} }
} }
) )
self?.becomeFirstResponder() self?.becomeFirstResponder()
}, },
afterClosed: { [weak self] in self?.becomeFirstResponder() } afterClosed: { [weak self] in
self?.becomeFirstResponder()
}
) )
) )
self.present(modal, animated: true) self.present(modal, animated: true)

@ -473,6 +473,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
inputTextView.resignFirstResponder() inputTextView.resignFirstResponder()
} }
@discardableResult
override func becomeFirstResponder() -> Bool { override func becomeFirstResponder() -> Bool {
inputTextView.becomeFirstResponder() inputTextView.becomeFirstResponder()
} }

@ -10,6 +10,7 @@ struct MessageInfoScreen: View {
@EnvironmentObject var host: HostWrapper @EnvironmentObject var host: HostWrapper
@State var index = 1 @State var index = 1
@State var feedbackMessage: String? = nil
static private let cornerRadius: CGFloat = 17 static private let cornerRadius: CGFloat = 17
@ -32,6 +33,9 @@ struct MessageInfoScreen: View {
messageViewModel: messageViewModel, messageViewModel: messageViewModel,
dependencies: dependencies dependencies: dependencies
) )
.clipShape(
RoundedRectangle(cornerRadius: Self.cornerRadius)
)
.background( .background(
RoundedRectangle(cornerRadius: Self.cornerRadius) RoundedRectangle(cornerRadius: Self.cornerRadius)
.fill( .fill(
@ -50,7 +54,6 @@ struct MessageInfoScreen: View {
.padding(.bottom, Values.verySmallSpacing) .padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing) .padding(.horizontal, Values.largeSpacing)
if isMessageFailed { if isMessageFailed {
let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo( let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo(
variant: messageViewModel.variant, variant: messageViewModel.variant,
@ -309,8 +312,17 @@ struct MessageInfoScreen: View {
let tintColor: ThemeValue = actions[index].themeColor let tintColor: ThemeValue = actions[index].themeColor
Button( Button(
action: { action: {
actions[index].work() actions[index].work() {
dismiss() switch (actions[index].shouldDismissInfoScreen, actions[index].feedback) {
case (false, _): break
case (true, .some):
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
dismiss()
})
default: dismiss()
}
}
feedbackMessage = actions[index].feedback
}, },
label: { label: {
HStack(spacing: Values.largeSpacing) { HStack(spacing: Values.largeSpacing) {
@ -353,6 +365,7 @@ struct MessageInfoScreen: View {
} }
} }
.backgroundColor(themeColor: .backgroundPrimary) .backgroundColor(themeColor: .backgroundPrimary)
.toastView(message: $feedbackMessage)
} }
private func showMediaFullScreen(attachment: Attachment) { private func showMediaFullScreen(attachment: Attachment) {

@ -2,6 +2,7 @@
import SwiftUI import SwiftUI
import Combine import Combine
import NaturalLanguage
public struct ToastModifier: ViewModifier { public struct ToastModifier: ViewModifier {
@Binding var message: String? @Binding var message: String?
@ -34,7 +35,17 @@ public struct ToastModifier: ViewModifier {
} }
workItem = task workItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: task)
let duration: TimeInterval = {
guard let message: String = message else { return 1.5 }
let tokenizer = NLTokenizer(unit: .word)
tokenizer.string = message
let wordCount = tokenizer.tokens(for: message.startIndex..<message.endIndex).count
return min(1.5 + Double(wordCount - 1) * 0.1 , 5)
}()
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: task)
} }
private func dismissToast() { private func dismissToast() {
@ -61,24 +72,18 @@ public struct ToastView_SwiftUI: View {
VStack( VStack(
spacing: 0 spacing: 0
) { ) {
ZStack { Text(message)
Capsule() .font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .toast_background) .foregroundColor(themeColor: .textPrimary)
.multilineTextAlignment(.center)
Text(message) .padding(.horizontal, Values.largeSpacing)
.font(.system(size: Values.verySmallFontSize)) .frame(height: Self.height)
.foregroundColor(themeColor: .textPrimary) .background(
.multilineTextAlignment(.center) Capsule()
.frame(maxWidth: .infinity) .foregroundColor(themeColor: .toast_background)
.padding(.horizontal, Values.mediumSpacing) )
}
.frame(
width: Self.width,
height: Self.height
)
} }
.frame( .frame(
maxWidth: .infinity,
maxHeight: .infinity, maxHeight: .infinity,
alignment: .bottom alignment: .bottom
) )

Loading…
Cancel
Save