Quote reply icons for polls

This commit is contained in:
kate-signal
2025-10-27 09:26:37 -04:00
committed by GitHub
parent 57b2c97688
commit 51f75d773b
16 changed files with 149 additions and 18 deletions

View File

@@ -285,7 +285,19 @@ public class QuotedMessageView: ManualStackViewWithLayer {
switch displayTextValue {
case .text(let text):
labelText = .text(text)
if state.quotedReplyModel.originalContent.isPoll {
let pollIcon = SignalSymbol.poll.attributedString(
dynamicTypeBaseSize: quotedTextFont.pointSize
) + " "
let pollPrefix = OWSLocalizedString(
"POLL_LABEL",
comment: "Label specifying the message type as a poll"
) + ": "
labelText = .attributedText(pollIcon + NSAttributedString(string: pollPrefix + text))
} else {
labelText = .text(text)
}
textAlignment = text.naturalTextAlignment
case .attributedText(let attributedText):
let mutableText = NSMutableAttributedString(attributedString: attributedText)

View File

@@ -222,7 +222,10 @@ private class QuotedMessageSnippetView: UIView {
let label = UILabel()
let attributedText: NSAttributedString
if let displayableQuotedText, !displayableQuotedText.displayTextValue.isEmpty {
if let displayableQuotedText,
!displayableQuotedText.displayTextValue.isEmpty,
!quotedMessage.content.isPoll
{
let config = HydratedMessageBody.DisplayConfiguration.quotedReply(
font: Layout.quotedTextFont,
textColor: .fixed(.Signal.label)
@@ -271,6 +274,35 @@ private class QuotedMessageSnippetView: UIView {
.foregroundColor: UIColor.Signal.secondaryLabel
]
)
} else if quotedMessage.content.isPoll {
switch quotedMessage.content {
case .poll(let pollQuestion):
let pollIcon = SignalSymbol.poll.attributedString(dynamicTypeBaseSize: Layout.fileTypeFont.pointSize) + " "
let pollPrefix = OWSLocalizedString(
"POLL_LABEL",
comment: "Label specifying the message type as a poll"
) + ": "
attributedText = pollIcon + NSAttributedString(
string: pollPrefix + pollQuestion,
attributes: [
.font: Layout.fileTypeFont,
.foregroundColor: UIColor.Signal.secondaryLabel
]
)
default:
owsFailDebug("Quoted message is poll but there's no poll")
attributedText = NSAttributedString(
string: NSLocalizedString(
"QUOTED_REPLY_TYPE_ATTACHMENT",
comment: "Indicates this message is a quoted reply to an attachment of unknown type."
),
attributes: [
.font: Layout.fileTypeFont,
.foregroundColor: UIColor.Signal.secondaryLabel
]
)
}
} else {
attributedText = NSAttributedString(
string: NSLocalizedString(
@@ -479,7 +511,7 @@ private class QuotedMessageSnippetView: UIView {
imageView.contentMode = .scaleAspectFit
thumbnailView = imageView
case .payment, .text, .viewOnce, .contactShare, .storyReactionEmoji:
case .payment, .text, .viewOnce, .contactShare, .storyReactionEmoji, .poll:
break
}
@@ -581,7 +613,7 @@ private class QuotedMessageSnippetView: UIView {
return (attachment.mimeType, reference.renderingFlag == .shouldLoop)
case .edit(_, _, let innerContent):
return mimeTypeAndIsLooping(innerContent)
case .giftBadge, .text, .payment, .attachmentStub, .viewOnce, .contactShare, .storyReactionEmoji:
case .giftBadge, .text, .payment, .attachmentStub, .viewOnce, .contactShare, .storyReactionEmoji, .poll:
return nil
}
}
@@ -635,7 +667,7 @@ private class QuotedMessageSnippetView: UIView {
return reference.sourceFilename
case .edit(_, _, let innerContent):
return sourceFilenameForSnippet(innerContent)
case .giftBadge, .text, .payment, .contactShare, .viewOnce, .storyReactionEmoji:
case .giftBadge, .text, .payment, .contactShare, .viewOnce, .storyReactionEmoji, .poll:
return nil
}
}

View File

@@ -598,6 +598,8 @@ class BackupArchiveTSMessageContentsArchiver: BackupArchiveProtoStreamWriter {
quote.type = .giftBadge
} else if quotedMessage.isTargetMessageViewOnce {
quote.type = .viewOnce
} else if quotedMessage.isPoll {
quote.type = .poll
} else {
guard didArchiveText || didArchiveAttachments else {
// NORMAL-type quotes must have either text or attachments, lest

View File

@@ -12,6 +12,7 @@ extension DraftQuotedReplyModel {
public let originalMessageAuthorAddress: SignalServiceAddress
public let originalMessageIsGiftBadge: Bool
public let originalMessageIsViewOnce: Bool
public let originalMessageIsPoll: Bool
public let threadUniqueId: String
public let quoteBody: MessageBody?

View File

@@ -34,6 +34,8 @@ public class DraftQuotedReplyModel {
/// The original message is a story reaction emoji
case storyReactionEmoji(String)
case poll(String)
// MARK: - Attachment types
/// The original message had an attachment, but it could not
@@ -84,6 +86,15 @@ public class DraftQuotedReplyModel {
}
}
public var isPoll: Bool {
switch self {
case .poll:
return true
default:
return false
}
}
public var isRemotelySourced: Bool {
switch self {
case .edit(_, let quotedMessage, _):
@@ -231,6 +242,9 @@ public class DraftQuotedReplyModel {
emoji
)
return MessageBody(text: text, ranges: .empty)
case .poll(let pollQuestion):
// Poll question should be the message body of the draft reply.
return MessageBody(text: pollQuestion, ranges: .empty)
}
}
}
@@ -248,7 +262,7 @@ extension DraftQuotedReplyModel: Equatable {
extension DraftQuotedReplyModel.Content: Equatable {
public static func == (lhs: DraftQuotedReplyModel.Content, rhs: DraftQuotedReplyModel.Content) -> Bool {
switch (lhs, rhs) {
case (.giftBadge, .giftBadge), (.viewOnce, .viewOnce):
case (.giftBadge, .giftBadge), (.viewOnce, .viewOnce), (.poll, .poll):
return true
case let (.payment(lhsBody), .payment(rhsBody)):
return lhsBody == rhsBody
@@ -281,6 +295,7 @@ extension DraftQuotedReplyModel.Content: Equatable {
(.attachment, _),
(.edit, _),
(.storyReactionEmoji, _),
(.poll, _),
(_, .giftBadge),
(_, .payment),
(_, .text),
@@ -289,7 +304,8 @@ extension DraftQuotedReplyModel.Content: Equatable {
(_, .attachmentStub),
(_, .attachment),
(_, .edit),
(_, .storyReactionEmoji):
(_, .storyReactionEmoji),
(_, .poll):
return false
}
}

View File

@@ -108,7 +108,8 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodySource: .remote,
receivedQuotedAttachmentInfo: nil,
isGiftBadge: true,
isTargetMessageViewOnce: false
isTargetMessageViewOnce: false,
isPoll: false
))
}
@@ -168,7 +169,8 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodySource: .remote,
receivedQuotedAttachmentInfo: attachmentInfo?.info,
isGiftBadge: false,
isTargetMessageViewOnce: false
isTargetMessageViewOnce: false,
isPoll: false
)
}
@@ -214,13 +216,15 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodySource: .local,
receivedQuotedAttachmentInfo: nil,
isGiftBadge: false,
isTargetMessageViewOnce: true
isTargetMessageViewOnce: true,
isPoll: false
))
}
let body: String?
let bodyRanges: MessageBodyRanges?
var isGiftBadge: Bool
var isPoll: Bool
if originalMessage is OWSPaymentMessage {
// This really should recalculate the string from payment metadata.
@@ -228,16 +232,19 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
body = quoteProto.text
bodyRanges = nil
isGiftBadge = false
isPoll = false
} else if let messageBody = originalMessage.body?.nilIfEmpty {
body = messageBody
bodyRanges = originalMessage.bodyRanges
isGiftBadge = false
isPoll = originalMessage.isPoll
} else if let contactName = originalMessage.contactShare?.name.displayName.nilIfEmpty {
// Contact share bodies are special-cased in OWSQuotedReplyModel
// We need to account for that here.
body = "👤 " + contactName
bodyRanges = nil
isGiftBadge = false
isPoll = false
} else if let storyReactionEmoji = originalMessage.storyReactionEmoji?.nilIfEmpty {
let formatString: String = {
if (authorAddress.isLocalAddress) {
@@ -255,10 +262,12 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
body = String(format: formatString, storyReactionEmoji)
bodyRanges = nil
isGiftBadge = false
isPoll = false
} else {
isGiftBadge = originalMessage.giftBadge != nil
body = nil
bodyRanges = nil
isPoll = false
}
let attachmentBuilder = self.attachmentBuilder(
@@ -285,7 +294,8 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodySource: .local,
receivedQuotedAttachmentInfo: attachmentInfo?.info,
isGiftBadge: isGiftBadge,
isTargetMessageViewOnce: false
isTargetMessageViewOnce: false,
isPoll: isPoll
)
}
@@ -536,6 +546,14 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
return createDraftReply(content: .storyReactionEmoji(storyReactionEmoji))
}
if originalMessage.isPoll {
guard let body = originalMessage.body else {
owsFailDebug("Poll message has no question body.")
return nil
}
return createDraftReply(content: .poll(body))
}
return createTextDraftReplyOrNil()
}
@@ -627,6 +645,7 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
originalMessageAuthorAddress: draft.originalMessageAuthorAddress,
originalMessageIsGiftBadge: draft.content.isGiftBadge,
originalMessageIsViewOnce: draft.content.isViewOnce,
originalMessageIsPoll: draft.content.isPoll,
threadUniqueId: draft.threadUniqueId,
quoteBody: draft.bodyForSending,
attachment: nil,
@@ -692,6 +711,7 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
originalMessageAuthorAddress: draft.originalMessageAuthorAddress,
originalMessageIsGiftBadge: draft.content.isGiftBadge,
originalMessageIsViewOnce: draft.content.isViewOnce,
originalMessageIsPoll: draft.content.isPoll,
threadUniqueId: draft.threadUniqueId,
quoteBody: draft.bodyForSending,
attachment: quoteAttachment,
@@ -728,7 +748,8 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodySource: .remote,
receivedQuotedAttachmentInfo: nil,
isGiftBadge: false,
isTargetMessageViewOnce: false
isTargetMessageViewOnce: false,
isPoll: false
))
}
@@ -742,7 +763,8 @@ public class QuotedReplyManagerImpl: QuotedReplyManager {
bodyRanges: body?.ranges,
quotedAttachmentForSending: attachmentInfo?.info,
isGiftBadge: draft.originalMessageIsGiftBadge,
isTargetMessageViewOnce: draft.originalMessageIsViewOnce
isTargetMessageViewOnce: draft.originalMessageIsViewOnce,
isPoll: draft.originalMessageIsPoll
)
}

View File

@@ -43,6 +43,7 @@ open class QuotedReplyManagerMock: QuotedReplyManager {
originalMessageAuthorAddress: draft.originalMessageAuthorAddress,
originalMessageIsGiftBadge: draft.content.isGiftBadge,
originalMessageIsViewOnce: draft.content.isViewOnce,
originalMessageIsPoll: draft.content.isPoll,
threadUniqueId: draft.threadUniqueId,
quoteBody: draft.bodyForSending,
attachment: nil,

View File

@@ -120,7 +120,8 @@ typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) {
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
quotedAttachmentForSending:(nullable OWSAttachmentInfo *)attachmentInfo
isGiftBadge:(BOOL)isGiftBadge
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce;
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce
isPoll:(BOOL)isPoll;
// used when receiving quoted messages. Do not call directly outside AttachmentManager.
- (instancetype)initWithTimestamp:(uint64_t)timestamp
@@ -130,7 +131,8 @@ typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) {
bodySource:(TSQuotedMessageContentSource)bodySource
receivedQuotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo
isGiftBadge:(BOOL)isGiftBadge
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce;
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce
isPoll:(BOOL)isPoll;
// used when restoring quoted messages from backups
+ (instancetype)quotedMessageFromBackupWithTargetMessageTimestamp:(nullable NSNumber *)timestamp

View File

@@ -106,6 +106,7 @@ NS_ASSUME_NONNULL_BEGIN
receivedQuotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo
isGiftBadge:(BOOL)isGiftBadge
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce
isPoll:(BOOL)isPoll
{
OWSAssertDebug(authorAddress.isValid);
@@ -122,6 +123,7 @@ NS_ASSUME_NONNULL_BEGIN
_quotedAttachment = attachmentInfo;
_isGiftBadge = isGiftBadge;
_isTargetMessageViewOnce = isTargetMessageViewOnce;
_isPoll = isPoll;
return self;
}
@@ -134,6 +136,7 @@ NS_ASSUME_NONNULL_BEGIN
quotedAttachmentForSending:(nullable OWSAttachmentInfo *)attachmentInfo
isGiftBadge:(BOOL)isGiftBadge
isTargetMessageViewOnce:(BOOL)isTargetMessageViewOnce
isPoll:(BOOL)isPoll
{
OWSAssertDebug(authorAddress.isValid);
@@ -150,6 +153,7 @@ NS_ASSUME_NONNULL_BEGIN
_quotedAttachment = attachmentInfo;
_isGiftBadge = isGiftBadge;
_isTargetMessageViewOnce = isTargetMessageViewOnce;
_isPoll = isPoll;
return self;
}
@@ -203,7 +207,8 @@ NS_ASSUME_NONNULL_BEGIN
bodySource:bodySource
receivedQuotedAttachmentInfo:attachmentInfo
isGiftBadge:isGiftBadge
isTargetMessageViewOnce:isTargetMessageViewOnce];
isTargetMessageViewOnce:isTargetMessageViewOnce
isPoll:isPoll];
// TODO (KC): add polls prefix for backup quoted messages when its implemented
}

View File

@@ -90,6 +90,7 @@ public enum SignalSymbol: Character {
case play = "\u{E067}"
case playSquare = "\u{E068}"
case playRectangle = "\u{E069}"
case poll = "\u{E082}"
case reply = "\u{E06D}"
case safetyNumber = "\u{E06F}"
case timer = "\u{E073}"
@@ -147,6 +148,8 @@ public enum SignalSymbol: Character {
case light
case regular
case bold
case medium
case thin
fileprivate var fontName: String {
switch self {
@@ -156,6 +159,10 @@ public enum SignalSymbol: Character {
return "SignalSymbols-Regular"
case .bold:
return "SignalSymbols-Bold"
case .medium:
return "SignalSymbols-Medium"
case .thin:
return "SignalSymbols-Thin"
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -68,6 +68,8 @@ public class QuotedReplyModel {
/// Used if the story has expired; we do not retain a copy.
case expiredStory
case poll(String)
// MARK: - Convenience
public var isGiftBadge: Bool {
@@ -88,6 +90,15 @@ public class QuotedReplyModel {
}
}
public var isPoll: Bool {
switch self {
case .poll:
return true
default:
return false
}
}
public var attachmentMimeType: String? {
switch self {
case .text(_):
@@ -106,6 +117,8 @@ public class QuotedReplyModel {
return nil
case .expiredStory:
return nil
case .poll:
return nil
}
}
@@ -127,6 +140,8 @@ public class QuotedReplyModel {
return nil
case .expiredStory:
return nil
case .poll:
return nil
}
}
}
@@ -164,6 +179,8 @@ public class QuotedReplyModel {
),
ranges: .empty
)
case .poll(let pollQuestion):
return MessageBody(text: pollQuestion, ranges: .empty)
}
}
@@ -185,6 +202,8 @@ public class QuotedReplyModel {
return nil
case .expiredStory:
return nil
case .poll:
return nil
}
}
@@ -207,6 +226,8 @@ public class QuotedReplyModel {
return true
case .expiredStory:
return false
case .poll:
return false
}
}
@@ -344,6 +365,14 @@ public class QuotedReplyModel {
return buildQuotedReplyModel(originalContent: .giftBadge)
}
if quotedMessage.isPoll {
guard let pollQuestion = originalMessageBody?.text else {
owsFailDebug("Quoted message is poll but no question found")
return buildQuotedReplyModel(originalContent: .text(originalMessageBody))
}
return buildQuotedReplyModel(originalContent: .poll(pollQuestion))
}
if quotedMessage.isTargetMessageViewOnce {
return buildQuotedReplyModel(originalContent: .text(.init(
text: OWSLocalizedString(
@@ -480,7 +509,8 @@ extension QuotedReplyModel.OriginalContent: Equatable {
return false
case (.expiredStory, .expiredStory):
return true
case (.poll, .poll):
return true
case
(.text, _),
(.giftBadge, _),
@@ -489,7 +519,8 @@ extension QuotedReplyModel.OriginalContent: Equatable {
(.attachment, _),
(.mediaStory, _),
(.textStory, _),
(.expiredStory, _):
(.expiredStory, _),
(.poll, _):
return false
}
}