mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 14:15:55 +00:00
ios: reactions UI (#2442)
* ios: reactions UI * remove JSON * remove print * align reactions, show all allowed reactions in menu * move react to the menu top * ios: update preference texts * always allow removing reactions, reduce spacing * revent allow removing (backend does not allow it anyway)
This commit is contained in:
committed by
GitHub
parent
a059739210
commit
904405ebee
@@ -1,20 +1,11 @@
|
||||
//import UIKit
|
||||
import SimpleXChat
|
||||
|
||||
let s = """
|
||||
{
|
||||
"contactConnection" : {
|
||||
"contactConnection" : {
|
||||
"viaContactUri" : false,
|
||||
"pccConnId" : 456,
|
||||
"pccAgentConnId" : "cTdjbmR4ZzVzSmhEZHdzMQ==",
|
||||
"pccConnStatus" : "new",
|
||||
"updatedAt" : "2022-04-24T11:59:23.703162Z",
|
||||
"createdAt" : "2022-04-24T11:59:23.703162Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
let s =
|
||||
"""
|
||||
{}
|
||||
"""
|
||||
//let s = "\"2022-04-24T11:59:23.703162Z\""
|
||||
//let json = getJSONDecoder()
|
||||
//let d = s.data(using: .utf8)!
|
||||
//print (try! json.decode(ChatInfo.self, from: d))
|
||||
let json = getJSONDecoder()
|
||||
let d = s.data(using: .utf8)!
|
||||
print (try! json.decode(APIResponse.self, from: d))
|
||||
|
||||
@@ -238,16 +238,9 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
let ci = reversedChatItems[i]
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
self.reversedChatItems[i] = cItem
|
||||
self.reversedChatItems[i].viewTimestamp = .now
|
||||
// on some occasions the confirmation of message being accepted by the server (tick)
|
||||
// arrives earlier than the response from API, and item remains without tick
|
||||
if case .sndNew = cItem.meta.itemStatus {
|
||||
self.reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
|
||||
}
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
@@ -264,7 +257,30 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
|
||||
let ci = reversedChatItems[i]
|
||||
reversedChatItems[i] = cItem
|
||||
reversedChatItems[i].viewTimestamp = .now
|
||||
// on some occasions the confirmation of message being accepted by the server (tick)
|
||||
// arrives earlier than the response from API, and item remains without tick
|
||||
if case .sndNew = cItem.meta.itemStatus {
|
||||
reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus
|
||||
}
|
||||
}
|
||||
|
||||
private func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
}
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
if cItem.isRcvNew {
|
||||
decreaseUnreadCounter(cInfo)
|
||||
@@ -277,7 +293,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// remove from current chat
|
||||
if chatId == cInfo.id {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
_ = withAnimation {
|
||||
self.reversedChatItems.remove(at: i)
|
||||
}
|
||||
@@ -357,7 +373,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
|
||||
if let cItem = aboveItem {
|
||||
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
markCurrentChatRead(fromIndex: i)
|
||||
_updateChat(cInfo.id) { chat in
|
||||
var unreadBelow = 0
|
||||
@@ -405,7 +421,7 @@ final class ChatModel: ObservableObject {
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
// update current chat
|
||||
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
markChatItemRead_(i)
|
||||
}
|
||||
}
|
||||
@@ -450,7 +466,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == ci.id }), i < reversedChatItems.count - 1 {
|
||||
if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 {
|
||||
return reversedChatItems[i + 1]
|
||||
} else {
|
||||
return nil
|
||||
|
||||
@@ -343,6 +343,12 @@ func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, reaction: MsgReaction, add: Bool) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, reaction: reaction, add: add), bgDelay: msgDelay)
|
||||
if case let .chatItemReaction(_, reaction, _) = r { return reaction.chatReaction.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
@@ -1264,6 +1270,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
case let .chatItemUpdated(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .chatItemReaction(user, r, _):
|
||||
if active(user) {
|
||||
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
|
||||
}
|
||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||
if !active(user) {
|
||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||
|
||||
@@ -444,6 +444,7 @@ struct ChatView: View {
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
var ci: ChatItem
|
||||
var showMember: Bool = false
|
||||
var maxWidth: CGFloat
|
||||
@@ -470,11 +471,11 @@ struct ChatView: View {
|
||||
set: { _ in }
|
||||
)
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
|
||||
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
|
||||
if ci.reactions.count > 0 {
|
||||
chatItemReactions(ci.reactions)
|
||||
if ci.content.msgContent != nil && ci.meta.itemDeleted == nil && ci.reactions.count > 0 {
|
||||
chatItemReactions()
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
@@ -505,28 +506,42 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func chatItemReactions(_ reactions: [CIReaction]) -> some View {
|
||||
private func chatItemReactions() -> some View {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(reactions, id: \.reaction) { r in
|
||||
HStack(spacing: 4) {
|
||||
ForEach(ci.reactions, id: \.reaction) { r in
|
||||
let v = HStack(spacing: 4) {
|
||||
switch r.reaction {
|
||||
case let .emoji(emoji): Text(emoji).font(.caption)
|
||||
case let .emoji(emoji): Text(emoji.rawValue).font(.caption)
|
||||
case .unknown: EmptyView()
|
||||
}
|
||||
if r.totalReacted > 1 {
|
||||
Text("\(r.totalReacted)").font(.caption).foregroundColor(.secondary)
|
||||
Text("\(r.totalReacted)")
|
||||
.font(.caption)
|
||||
.fontWeight(r.userReacted ? .bold : .light)
|
||||
.foregroundColor(r.userReacted ? .accentColor : .secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
.background(!r.userReacted ? Color.clear : colorScheme == .dark ? sentColorDark : sentColorLight)
|
||||
.cornerRadius(16)
|
||||
|
||||
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
|
||||
v.onTapGesture {
|
||||
setReaction(r.reaction, add: !r.userReacted)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func menu(live: Bool) -> [UIAction] {
|
||||
var menu: [UIAction] = []
|
||||
private func menu(live: Bool) -> [UIMenuElement] {
|
||||
var menu: [UIMenuElement] = []
|
||||
if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed {
|
||||
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction && developerTools,
|
||||
let rm = reactionUIMenu() {
|
||||
menu.append(rm)
|
||||
}
|
||||
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
|
||||
menu.append(replyUIAction())
|
||||
}
|
||||
@@ -586,7 +601,42 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func reactionUIMenu() -> UIMenu? {
|
||||
let rs = MsgReaction.values.compactMap { r in
|
||||
ci.reactions.contains(where: { $0.userReacted && $0.reaction == r })
|
||||
? nil
|
||||
: UIAction(title: r.text) { _ in setReaction(r, add: true) }
|
||||
}
|
||||
if rs.count > 0 {
|
||||
return UIMenu(
|
||||
title: NSLocalizedString("React...", comment: "chat item menu"),
|
||||
image: UIImage(systemName: "hand.thumbsup"),
|
||||
children: rs
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func setReaction(_ r: MsgReaction, add: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let chatItem = try await apiChatItemReaction(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
itemId: ci.id,
|
||||
reaction: r,
|
||||
add: add
|
||||
)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiChatItemReaction error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shareUIAction() -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Share", comment: "chat item action"),
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ContactPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var contact: Contact
|
||||
@State var featuresAllowed: ContactFeaturesAllowed
|
||||
@State var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
@@ -24,6 +25,9 @@ struct ContactPreferencesView: View {
|
||||
List {
|
||||
timedMessagesFeatureSection()
|
||||
featureSection(.fullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, $featuresAllowed.fullDelete)
|
||||
if developerTools {
|
||||
featureSection(.reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, $featuresAllowed.reactions)
|
||||
}
|
||||
featureSection(.voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, $featuresAllowed.voice)
|
||||
featureSection(.calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, $featuresAllowed.calls)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct GroupPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var preferences: FullGroupPreferences
|
||||
@State var currentPreferences: FullGroupPreferences
|
||||
@@ -25,6 +26,9 @@ struct GroupPreferencesView: View {
|
||||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable)
|
||||
if developerTools {
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
}
|
||||
featureSection(.voice, $preferences.voice.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct PreferencesView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State var profile: LocalProfile
|
||||
@State var preferences: FullPreferences
|
||||
@State var currentPreferences: FullPreferences
|
||||
@@ -20,6 +21,9 @@ struct PreferencesView: View {
|
||||
List {
|
||||
timedMessagesFeatureSection($preferences.timedMessages.allow)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.allow)
|
||||
if developerTools {
|
||||
featureSection(.reactions, $preferences.reactions.allow)
|
||||
}
|
||||
featureSection(.voice, $preferences.voice.allow)
|
||||
featureSection(.calls, $preferences.calls.allow)
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ public enum ChatCommand {
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, reaction: MsgReaction, add: Bool)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
@@ -148,6 +149,7 @@ public enum ChatCommand {
|
||||
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
|
||||
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
||||
case let .apiChatItemReaction(type, id, itemId, reaction, add): return "/_reaction \(ref(type, id)) \(itemId) \(reaction.cmdString) \(onOff(add))"
|
||||
case .apiGetNtfToken: return "/_ntf get "
|
||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
@@ -253,6 +255,7 @@ public enum ChatCommand {
|
||||
case .apiUpdateChatItem: return "apiUpdateChatItem"
|
||||
case .apiDeleteChatItem: return "apiDeleteChatItem"
|
||||
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
|
||||
case .apiChatItemReaction: return "apiChatItemReaction"
|
||||
case .apiGetNtfToken: return "apiGetNtfToken"
|
||||
case .apiRegisterToken: return "apiRegisterToken"
|
||||
case .apiVerifyToken: return "apiVerifyToken"
|
||||
@@ -373,7 +376,7 @@ public enum ChatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
struct APIResponse: Decodable {
|
||||
public struct APIResponse: Decodable {
|
||||
var resp: ChatResponse
|
||||
}
|
||||
|
||||
@@ -430,6 +433,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case newChatItem(user: User, chatItem: AChatItem)
|
||||
case chatItemStatusUpdated(user: User, chatItem: AChatItem)
|
||||
case chatItemUpdated(user: User, chatItem: AChatItem)
|
||||
case chatItemReaction(user: User, reaction: ACIReaction, added: Bool)
|
||||
case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
|
||||
case contactsList(user: User, contacts: [Contact])
|
||||
// group events
|
||||
@@ -547,6 +551,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .newChatItem: return "newChatItem"
|
||||
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
|
||||
case .chatItemUpdated: return "chatItemUpdated"
|
||||
case .chatItemReaction: return "chatItemReaction"
|
||||
case .chatItemDeleted: return "chatItemDeleted"
|
||||
case .contactsList: return "contactsList"
|
||||
case .groupCreated: return "groupCreated"
|
||||
@@ -663,6 +668,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemReaction(u, reaction, added): return withUser(u, "added: \(added)\n\(String(describing: reaction))")
|
||||
case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)")
|
||||
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
|
||||
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
|
||||
@@ -183,12 +183,20 @@ public typealias ChatId = String
|
||||
public struct FullPreferences: Decodable, Equatable {
|
||||
public var timedMessages: TimedMessagesPreference
|
||||
public var fullDelete: SimplePreference
|
||||
public var reactions: SimplePreference
|
||||
public var voice: SimplePreference
|
||||
public var calls: SimplePreference
|
||||
|
||||
public init(timedMessages: TimedMessagesPreference, fullDelete: SimplePreference, voice: SimplePreference, calls: SimplePreference) {
|
||||
public init(
|
||||
timedMessages: TimedMessagesPreference,
|
||||
fullDelete: SimplePreference,
|
||||
reactions: SimplePreference,
|
||||
voice: SimplePreference,
|
||||
calls: SimplePreference
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.calls = calls
|
||||
}
|
||||
@@ -196,6 +204,7 @@ public struct FullPreferences: Decodable, Equatable {
|
||||
public static let sampleData = FullPreferences(
|
||||
timedMessages: TimedMessagesPreference(allow: .no),
|
||||
fullDelete: SimplePreference(allow: .no),
|
||||
reactions: SimplePreference(allow: .yes),
|
||||
voice: SimplePreference(allow: .yes),
|
||||
calls: SimplePreference(allow: .yes)
|
||||
)
|
||||
@@ -204,20 +213,35 @@ public struct FullPreferences: Decodable, Equatable {
|
||||
public struct Preferences: Codable {
|
||||
public var timedMessages: TimedMessagesPreference?
|
||||
public var fullDelete: SimplePreference?
|
||||
public var reactions: SimplePreference?
|
||||
public var voice: SimplePreference?
|
||||
public var calls: SimplePreference?
|
||||
|
||||
public init(timedMessages: TimedMessagesPreference?, fullDelete: SimplePreference?, voice: SimplePreference?, calls: SimplePreference?) {
|
||||
public init(
|
||||
timedMessages: TimedMessagesPreference?,
|
||||
fullDelete: SimplePreference?,
|
||||
reactions: SimplePreference?,
|
||||
voice: SimplePreference?,
|
||||
calls: SimplePreference?
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.calls = calls
|
||||
}
|
||||
|
||||
func copy(timedMessages: TimedMessagesPreference? = nil, fullDelete: SimplePreference? = nil, voice: SimplePreference? = nil, calls: SimplePreference? = nil) -> Preferences {
|
||||
func copy(
|
||||
timedMessages: TimedMessagesPreference? = nil,
|
||||
fullDelete: SimplePreference? = nil,
|
||||
reactions: SimplePreference? = nil,
|
||||
voice: SimplePreference? = nil,
|
||||
calls: SimplePreference? = nil
|
||||
) -> Preferences {
|
||||
Preferences(
|
||||
timedMessages: timedMessages ?? self.timedMessages,
|
||||
fullDelete: fullDelete ?? self.fullDelete,
|
||||
reactions: reactions ?? self.reactions,
|
||||
voice: voice ?? self.voice,
|
||||
calls: calls ?? self.calls
|
||||
)
|
||||
@@ -227,6 +251,7 @@ public struct Preferences: Codable {
|
||||
switch feature {
|
||||
case .timedMessages: return copy(timedMessages: TimedMessagesPreference(allow: allowed, ttl: param ?? timedMessages?.ttl))
|
||||
case .fullDelete: return copy(fullDelete: SimplePreference(allow: allowed))
|
||||
case .reactions: return copy(reactions: SimplePreference(allow: allowed))
|
||||
case .voice: return copy(voice: SimplePreference(allow: allowed))
|
||||
case .calls: return copy(calls: SimplePreference(allow: allowed))
|
||||
}
|
||||
@@ -235,6 +260,7 @@ public struct Preferences: Codable {
|
||||
public static let sampleData = Preferences(
|
||||
timedMessages: TimedMessagesPreference(allow: .no),
|
||||
fullDelete: SimplePreference(allow: .no),
|
||||
reactions: SimplePreference(allow: .yes),
|
||||
voice: SimplePreference(allow: .yes),
|
||||
calls: SimplePreference(allow: .yes)
|
||||
)
|
||||
@@ -244,6 +270,7 @@ public func fullPreferencesToPreferences(_ fullPreferences: FullPreferences) ->
|
||||
Preferences(
|
||||
timedMessages: fullPreferences.timedMessages,
|
||||
fullDelete: fullPreferences.fullDelete,
|
||||
reactions: fullPreferences.reactions,
|
||||
voice: fullPreferences.voice,
|
||||
calls: fullPreferences.calls
|
||||
)
|
||||
@@ -253,6 +280,7 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac
|
||||
Preferences(
|
||||
timedMessages: contactUserPreferences.timedMessages.userPreference.preference,
|
||||
fullDelete: contactUserPreferences.fullDelete.userPreference.preference,
|
||||
reactions: contactUserPreferences.reactions.userPreference.preference,
|
||||
voice: contactUserPreferences.voice.userPreference.preference,
|
||||
calls: contactUserPreferences.calls.userPreference.preference
|
||||
)
|
||||
@@ -386,17 +414,20 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey {
|
||||
public struct ContactUserPreferences: Decodable {
|
||||
public var timedMessages: ContactUserPreference<TimedMessagesPreference>
|
||||
public var fullDelete: ContactUserPreference<SimplePreference>
|
||||
public var reactions: ContactUserPreference<SimplePreference>
|
||||
public var voice: ContactUserPreference<SimplePreference>
|
||||
public var calls: ContactUserPreference<SimplePreference>
|
||||
|
||||
public init(
|
||||
timedMessages: ContactUserPreference<TimedMessagesPreference>,
|
||||
fullDelete: ContactUserPreference<SimplePreference>,
|
||||
reactions: ContactUserPreference<SimplePreference>,
|
||||
voice: ContactUserPreference<SimplePreference>,
|
||||
calls: ContactUserPreference<SimplePreference>
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.calls = calls
|
||||
}
|
||||
@@ -412,6 +443,11 @@ public struct ContactUserPreferences: Decodable {
|
||||
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .no)),
|
||||
contactPreference: SimplePreference(allow: .no)
|
||||
),
|
||||
reactions: ContactUserPreference<SimplePreference>(
|
||||
enabled: FeatureEnabled(forUser: true, forContact: true),
|
||||
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .yes)),
|
||||
contactPreference: SimplePreference(allow: .yes)
|
||||
),
|
||||
voice: ContactUserPreference<SimplePreference>(
|
||||
enabled: FeatureEnabled(forUser: true, forContact: true),
|
||||
userPreference: ContactUserPref<SimplePreference>.user(preference: SimplePreference(allow: .yes)),
|
||||
@@ -491,11 +527,10 @@ public protocol Feature {
|
||||
public enum ChatFeature: String, Decodable, Feature {
|
||||
case timedMessages
|
||||
case fullDelete
|
||||
case reactions
|
||||
case voice
|
||||
case calls
|
||||
|
||||
public var values: [ChatFeature] { [.fullDelete, .voice] }
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public var asymmetric: Bool {
|
||||
@@ -516,6 +551,7 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
switch self {
|
||||
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
|
||||
case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature")
|
||||
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
|
||||
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
|
||||
case .calls: return NSLocalizedString("Audio/video calls", comment: "chat feature")
|
||||
}
|
||||
@@ -525,6 +561,7 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
switch self {
|
||||
case .timedMessages: return "stopwatch"
|
||||
case .fullDelete: return "trash.slash"
|
||||
case .reactions: return "hand.thumbsup"
|
||||
case .voice: return "mic"
|
||||
case .calls: return "phone"
|
||||
}
|
||||
@@ -534,6 +571,7 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
switch self {
|
||||
case .timedMessages: return "stopwatch.fill"
|
||||
case .fullDelete: return "trash.slash.fill"
|
||||
case .reactions: return "hand.thumbsup.fill"
|
||||
case .voice: return "mic.fill"
|
||||
case .calls: return "phone.fill"
|
||||
}
|
||||
@@ -560,6 +598,12 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
case .yes: return "Allow irreversible message deletion only if your contact allows it to you."
|
||||
case .no: return "Contacts can mark messages for deletion; you will be able to view them."
|
||||
}
|
||||
case .reactions:
|
||||
switch allowed {
|
||||
case .always: return "Allow your contacts adding message reactions."
|
||||
case .yes: return "Allow message reactions only if your contact allows them."
|
||||
case .no: return "Prohibit message reactions."
|
||||
}
|
||||
case .voice:
|
||||
switch allowed {
|
||||
case .always: return "Allow your contacts to send voice messages."
|
||||
@@ -593,6 +637,14 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
: enabled.forContact
|
||||
? "Only your contact can irreversibly delete messages (you can mark them for deletion)."
|
||||
: "Irreversible message deletion is prohibited in this chat."
|
||||
case .reactions:
|
||||
return enabled.forUser && enabled.forContact
|
||||
? "Both you and your contact can add message reactions."
|
||||
: enabled.forUser
|
||||
? "Only you can add message reactions."
|
||||
: enabled.forContact
|
||||
? "Only your contact can add message reactions."
|
||||
: "Message reactions are prohibited in this chat."
|
||||
case .voice:
|
||||
return enabled.forUser && enabled.forContact
|
||||
? "Both you and your contact can send voice messages."
|
||||
@@ -616,11 +668,10 @@ public enum ChatFeature: String, Decodable, Feature {
|
||||
public enum GroupFeature: String, Decodable, Feature {
|
||||
case timedMessages
|
||||
case fullDelete
|
||||
case reactions
|
||||
case voice
|
||||
case directMessages
|
||||
|
||||
public var values: [GroupFeature] { [.directMessages, .fullDelete, .voice] }
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public var hasParam: Bool {
|
||||
@@ -635,6 +686,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
|
||||
case .directMessages: return NSLocalizedString("Direct messages", comment: "chat feature")
|
||||
case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature")
|
||||
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
|
||||
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
|
||||
}
|
||||
}
|
||||
@@ -644,6 +696,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .timedMessages: return "stopwatch"
|
||||
case .directMessages: return "arrow.left.and.right.circle"
|
||||
case .fullDelete: return "trash.slash"
|
||||
case .reactions: return "hand.thumbsup"
|
||||
case .voice: return "mic"
|
||||
}
|
||||
}
|
||||
@@ -653,6 +706,7 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .timedMessages: return "stopwatch.fill"
|
||||
case .directMessages: return "arrow.left.and.right.circle.fill"
|
||||
case .fullDelete: return "trash.slash.fill"
|
||||
case .reactions: return "hand.thumbsup.fill"
|
||||
case .voice: return "mic.fill"
|
||||
}
|
||||
}
|
||||
@@ -682,6 +736,11 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .on: return "Allow to irreversibly delete sent messages."
|
||||
case .off: return "Prohibit irreversible message deletion."
|
||||
}
|
||||
case .reactions:
|
||||
switch enabled {
|
||||
case .on: return "Allow message reactions."
|
||||
case .off: return "Prohibit messages reactions."
|
||||
}
|
||||
case .voice:
|
||||
switch enabled {
|
||||
case .on: return "Allow to send voice messages."
|
||||
@@ -705,6 +764,11 @@ public enum GroupFeature: String, Decodable, Feature {
|
||||
case .on: return "Group members can irreversibly delete sent messages."
|
||||
case .off: return "Irreversible message deletion is prohibited in this group."
|
||||
}
|
||||
case .reactions:
|
||||
switch enabled {
|
||||
case .on: return "Group members can add message reactions."
|
||||
case .off: return "Message reactions are prohibited in this group."
|
||||
}
|
||||
case .voice:
|
||||
switch enabled {
|
||||
case .on: return "Group members can send voice messages."
|
||||
@@ -750,13 +814,22 @@ public struct ContactFeaturesAllowed: Equatable {
|
||||
public var timedMessagesAllowed: Bool
|
||||
public var timedMessagesTTL: Int?
|
||||
public var fullDelete: ContactFeatureAllowed
|
||||
public var reactions: ContactFeatureAllowed
|
||||
public var voice: ContactFeatureAllowed
|
||||
public var calls: ContactFeatureAllowed
|
||||
|
||||
public init(timedMessagesAllowed: Bool, timedMessagesTTL: Int?, fullDelete: ContactFeatureAllowed, voice: ContactFeatureAllowed, calls: ContactFeatureAllowed) {
|
||||
public init(
|
||||
timedMessagesAllowed: Bool,
|
||||
timedMessagesTTL: Int?,
|
||||
fullDelete: ContactFeatureAllowed,
|
||||
reactions: ContactFeatureAllowed,
|
||||
voice: ContactFeatureAllowed,
|
||||
calls: ContactFeatureAllowed
|
||||
) {
|
||||
self.timedMessagesAllowed = timedMessagesAllowed
|
||||
self.timedMessagesTTL = timedMessagesTTL
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
self.calls = calls
|
||||
}
|
||||
@@ -765,6 +838,7 @@ public struct ContactFeaturesAllowed: Equatable {
|
||||
timedMessagesAllowed: false,
|
||||
timedMessagesTTL: nil,
|
||||
fullDelete: ContactFeatureAllowed.userDefault(.no),
|
||||
reactions: ContactFeatureAllowed.userDefault(.yes),
|
||||
voice: ContactFeatureAllowed.userDefault(.yes),
|
||||
calls: ContactFeatureAllowed.userDefault(.yes)
|
||||
)
|
||||
@@ -777,6 +851,7 @@ public func contactUserPrefsToFeaturesAllowed(_ contactUserPreferences: ContactU
|
||||
timedMessagesAllowed: allow == .yes || allow == .always,
|
||||
timedMessagesTTL: pref.preference.ttl,
|
||||
fullDelete: contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
|
||||
reactions: contactUserPrefToFeatureAllowed(contactUserPreferences.reactions),
|
||||
voice: contactUserPrefToFeatureAllowed(contactUserPreferences.voice),
|
||||
calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls)
|
||||
)
|
||||
@@ -798,6 +873,7 @@ public func contactFeaturesAllowedToPrefs(_ contactFeaturesAllowed: ContactFeatu
|
||||
Preferences(
|
||||
timedMessages: TimedMessagesPreference(allow: contactFeaturesAllowed.timedMessagesAllowed ? .yes : .no, ttl: contactFeaturesAllowed.timedMessagesTTL),
|
||||
fullDelete: contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
|
||||
reactions: contactFeatureAllowedToPref(contactFeaturesAllowed.reactions),
|
||||
voice: contactFeatureAllowedToPref(contactFeaturesAllowed.voice),
|
||||
calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls)
|
||||
)
|
||||
@@ -834,12 +910,20 @@ public struct FullGroupPreferences: Decodable, Equatable {
|
||||
public var timedMessages: TimedMessagesGroupPreference
|
||||
public var directMessages: GroupPreference
|
||||
public var fullDelete: GroupPreference
|
||||
public var reactions: GroupPreference
|
||||
public var voice: GroupPreference
|
||||
|
||||
public init(timedMessages: TimedMessagesGroupPreference, directMessages: GroupPreference, fullDelete: GroupPreference, voice: GroupPreference) {
|
||||
public init(
|
||||
timedMessages: TimedMessagesGroupPreference,
|
||||
directMessages: GroupPreference,
|
||||
fullDelete: GroupPreference,
|
||||
reactions: GroupPreference,
|
||||
voice: GroupPreference
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
self.directMessages = directMessages
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
}
|
||||
|
||||
@@ -847,6 +931,7 @@ public struct FullGroupPreferences: Decodable, Equatable {
|
||||
timedMessages: TimedMessagesGroupPreference(enable: .off),
|
||||
directMessages: GroupPreference(enable: .off),
|
||||
fullDelete: GroupPreference(enable: .off),
|
||||
reactions: GroupPreference(enable: .on),
|
||||
voice: GroupPreference(enable: .on)
|
||||
)
|
||||
}
|
||||
@@ -855,12 +940,20 @@ public struct GroupPreferences: Codable {
|
||||
public var timedMessages: TimedMessagesGroupPreference?
|
||||
public var directMessages: GroupPreference?
|
||||
public var fullDelete: GroupPreference?
|
||||
public var reactions: GroupPreference?
|
||||
public var voice: GroupPreference?
|
||||
|
||||
public init(timedMessages: TimedMessagesGroupPreference?, directMessages: GroupPreference?, fullDelete: GroupPreference?, voice: GroupPreference?) {
|
||||
public init(
|
||||
timedMessages: TimedMessagesGroupPreference?,
|
||||
directMessages: GroupPreference?,
|
||||
fullDelete: GroupPreference?,
|
||||
reactions: GroupPreference?,
|
||||
voice: GroupPreference?
|
||||
) {
|
||||
self.timedMessages = timedMessages
|
||||
self.directMessages = directMessages
|
||||
self.fullDelete = fullDelete
|
||||
self.reactions = reactions
|
||||
self.voice = voice
|
||||
}
|
||||
|
||||
@@ -868,6 +961,7 @@ public struct GroupPreferences: Codable {
|
||||
timedMessages: TimedMessagesGroupPreference(enable: .off),
|
||||
directMessages: GroupPreference(enable: .off),
|
||||
fullDelete: GroupPreference(enable: .off),
|
||||
reactions: GroupPreference(enable: .on),
|
||||
voice: GroupPreference(enable: .on)
|
||||
)
|
||||
}
|
||||
@@ -877,6 +971,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
|
||||
timedMessages: fullPreferences.timedMessages,
|
||||
directMessages: fullPreferences.directMessages,
|
||||
fullDelete: fullPreferences.fullDelete,
|
||||
reactions: fullPreferences.reactions,
|
||||
voice: fullPreferences.voice
|
||||
)
|
||||
}
|
||||
@@ -1086,6 +1181,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
switch feature {
|
||||
case .timedMessages: return cups.timedMessages.enabled.forUser
|
||||
case .fullDelete: return cups.fullDelete.enabled.forUser
|
||||
case .reactions: return cups.reactions.enabled.forUser
|
||||
case .voice: return cups.voice.enabled.forUser
|
||||
case .calls: return cups.calls.enabled.forUser
|
||||
}
|
||||
@@ -1094,6 +1190,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
switch feature {
|
||||
case .timedMessages: return prefs.timedMessages.on
|
||||
case .fullDelete: return prefs.fullDelete.on
|
||||
case .reactions: return prefs.reactions.on
|
||||
case .voice: return prefs.voice.on
|
||||
case .calls: return false
|
||||
}
|
||||
@@ -1249,6 +1346,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
switch feature {
|
||||
case .timedMessages: return mergedPreferences.timedMessages.contactPreference.allow != .no
|
||||
case .fullDelete: return mergedPreferences.fullDelete.contactPreference.allow != .no
|
||||
case .reactions: return mergedPreferences.reactions.contactPreference.allow != .no
|
||||
case .voice: return mergedPreferences.voice.contactPreference.allow != .no
|
||||
case .calls: return mergedPreferences.calls.contactPreference.allow != .no
|
||||
}
|
||||
@@ -1258,6 +1356,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
switch feature {
|
||||
case .timedMessages: return mergedPreferences.timedMessages.userPreference.preference.allow != .no
|
||||
case .fullDelete: return mergedPreferences.fullDelete.userPreference.preference.allow != .no
|
||||
case .reactions: return mergedPreferences.reactions.userPreference.preference.allow != .no
|
||||
case .voice: return mergedPreferences.voice.userPreference.preference.allow != .no
|
||||
case .calls: return mergedPreferences.calls.userPreference.preference.allow != .no
|
||||
}
|
||||
@@ -1802,24 +1901,26 @@ public struct AChatItem: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ACIReaction: Decodable {
|
||||
public var chatInfo: ChatInfo
|
||||
public var chatReaction: CIReaction
|
||||
}
|
||||
|
||||
public struct CIReaction: Decodable {
|
||||
public var chatDir: CIDirection
|
||||
public var chatItem: ChatItem
|
||||
public var sentAt: Date
|
||||
public var reaction: MsgReaction
|
||||
}
|
||||
|
||||
public struct ChatItem: Identifiable, Decodable {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, file: CIFile? = nil) {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) {
|
||||
self.chatDir = chatDir
|
||||
self.meta = meta
|
||||
self.content = content
|
||||
self.formattedText = formattedText
|
||||
self.quotedItem = quotedItem
|
||||
self.reactions = [] // [
|
||||
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: false, totalReacted: 1),
|
||||
// CIReaction(reaction: .emoji(emoji: "❤️"), userReacted: false, totalReacted: 1),
|
||||
// CIReaction(reaction: .emoji(emoji: "🚀"), userReacted: false, totalReacted: 3),
|
||||
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "👎"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "👀"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "🎉"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "😀"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "😕"), userReacted: true, totalReacted: 2),
|
||||
// ]
|
||||
self.reactions = reactions
|
||||
self.file = file
|
||||
}
|
||||
|
||||
@@ -1828,24 +1929,14 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
public var content: CIContent
|
||||
public var formattedText: [FormattedText]?
|
||||
public var quotedItem: CIQuote?
|
||||
public var reactions: [CIReaction] = [] // [
|
||||
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: false, totalReacted: 1),
|
||||
// CIReaction(reaction: .emoji(emoji: "❤️"), userReacted: false, totalReacted: 1),
|
||||
// CIReaction(reaction: .emoji(emoji: "🚀"), userReacted: false, totalReacted: 3),
|
||||
// CIReaction(reaction: .emoji(emoji: "👍"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "👎"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "👀"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "🎉"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "😀"), userReacted: true, totalReacted: 2),
|
||||
// CIReaction(reaction: .emoji(emoji: "😕"), userReacted: true, totalReacted: 2),
|
||||
// ]
|
||||
public var reactions: [CIReactionCount]
|
||||
public var file: CIFile?
|
||||
|
||||
public var viewTimestamp = Date.now
|
||||
public var isLiveDummy: Bool = false
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case chatDir, meta, content, formattedText, quotedItem, file
|
||||
case chatDir, meta, content, formattedText, quotedItem, reactions, file
|
||||
}
|
||||
|
||||
public var id: Int64 { meta.itemId }
|
||||
@@ -1920,6 +2011,10 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public var allowAddReaction: Bool {
|
||||
meta.itemDeleted == nil && !isLiveDummy && reactions.filter({ $0.userReacted }).count < 3
|
||||
}
|
||||
|
||||
public func autoReceiveFile() -> CIFile? {
|
||||
if let file = file,
|
||||
let mc = content.msgContent,
|
||||
@@ -2367,14 +2462,79 @@ public struct CIQuote: Decodable, ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIReaction: Decodable {
|
||||
public struct CIReactionCount: Decodable {
|
||||
public var reaction: MsgReaction
|
||||
public var userReacted: Bool
|
||||
public var totalReacted: Int
|
||||
}
|
||||
|
||||
public enum MsgReaction: Decodable, Hashable {
|
||||
case emoji(emoji: String)
|
||||
public enum MsgReaction: Hashable {
|
||||
case emoji(MREmojiChar)
|
||||
case unknown
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case let .emoji(emoji): return emoji.rawValue
|
||||
case .unknown: return ""
|
||||
}
|
||||
}
|
||||
|
||||
public var cmdString: String {
|
||||
switch self {
|
||||
case let .emoji(emoji): return emoji.cmdString
|
||||
case .unknown: return ""
|
||||
}
|
||||
}
|
||||
|
||||
public static var values: [MsgReaction] =
|
||||
MREmojiChar.allCases.map { .emoji($0) }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case emoji
|
||||
}
|
||||
}
|
||||
|
||||
public enum MREmojiChar: String, Codable, CaseIterable {
|
||||
case thumbsup = "👍"
|
||||
case thumbsdown = "👎"
|
||||
case smile = "😀"
|
||||
case celebration = "🎉"
|
||||
case confused = "😕"
|
||||
case heart = "❤"
|
||||
case launch = "🚀"
|
||||
case looking = "👀"
|
||||
|
||||
public var cmdString: String {
|
||||
switch self {
|
||||
case .thumbsup: return "+"
|
||||
case .thumbsdown: return "-"
|
||||
case .smile: return ")"
|
||||
case .celebration: return "!"
|
||||
case .confused: return "?"
|
||||
case .heart: return "*"
|
||||
case .launch: return "^"
|
||||
case .looking: return "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MsgReaction: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
do {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
||||
switch type {
|
||||
case "emoji":
|
||||
let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji)
|
||||
self = .emoji(emoji)
|
||||
default:
|
||||
self = .unknown
|
||||
}
|
||||
} catch {
|
||||
self = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIFile: Decodable {
|
||||
|
||||
@@ -91,9 +91,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
|
||||
CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts
|
||||
CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci
|
||||
CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts testView
|
||||
CRChatItemReaction u (ACIReaction _ _ chat reaction) added
|
||||
| showReactions -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction chat reaction added ts tz
|
||||
| otherwise -> []
|
||||
CRChatItemReaction u (ACIReaction _ _ chat reaction) added -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction showReactions chat reaction added ts tz
|
||||
CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"]
|
||||
CRBroadcastSent u mc n t -> ttyUser u $ viewSentBroadcast mc n ts t
|
||||
CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr
|
||||
@@ -514,8 +512,8 @@ viewItemDelete chat ChatItem {chatDir, meta, content = deletedContent} toItem by
|
||||
Just (AChatItem _ _ _ ci) -> chatItemDeletedText ci $ chatInfoMembership chat
|
||||
prohibited = [styled (colored Red) ("[unexpected message deletion, please report to developers]" :: String)]
|
||||
|
||||
viewItemReaction :: forall c d. ChatInfo c -> CIReaction c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
|
||||
viewItemReaction chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {chatDir = itemDir, content}, sentAt, reaction} added ts tz =
|
||||
viewItemReaction :: forall c d. Bool -> ChatInfo c -> CIReaction c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
|
||||
viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {chatDir = itemDir, content}, sentAt, reaction} added ts tz =
|
||||
case (chat, chatDir) of
|
||||
(DirectChat c, CIDirectRcv) -> case content of
|
||||
CIRcvMsgContent mc -> view from $ reactionMsg mc
|
||||
@@ -534,7 +532,9 @@ viewItemReaction chat CIReaction {chatDir, chatItem = CChatItem md ChatItem {cha
|
||||
(_, CIDirectSnd) -> [sentText]
|
||||
(_, CIGroupSnd) -> [sentText]
|
||||
where
|
||||
view from msg = viewReceivedReaction from msg reactionText ts $ utcToZonedTime tz sentAt
|
||||
view from msg
|
||||
| showReactions = viewReceivedReaction from msg reactionText ts $ utcToZonedTime tz sentAt
|
||||
| otherwise = []
|
||||
reactionText = plain $ (if added then "+ " else "- ") <> [emoji]
|
||||
MREmoji (MREmojiChar emoji) = reaction
|
||||
sentText = plain $ (if added then "added " else "removed ") <> [emoji]
|
||||
|
||||
Reference in New Issue
Block a user