NCChatMessage.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. //
  2. // SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. import SwiftyAttributes
  7. @objc extension NCChatMessage {
  8. public var isSystemMessage: Bool {
  9. return self.systemMessage != nil && !self.systemMessage.isEmpty
  10. }
  11. public var isEmojiMessage: Bool {
  12. return self.message != nil && self.message.containsOnlyEmoji && self.message.emojiCount <= 3
  13. }
  14. public var isUpdateMessage: Bool {
  15. return self.systemMessage == "message_deleted" ||
  16. self.systemMessage == "reaction" ||
  17. self.systemMessage == "reaction_revoked" ||
  18. self.systemMessage == "reaction_deleted" ||
  19. self.systemMessage == "poll_voted" ||
  20. self.systemMessage == "message_edited"
  21. }
  22. public var isDeletedMessage: Bool {
  23. return self.messageType == kMessageTypeCommentDeleted
  24. }
  25. public var isVoiceMessage: Bool {
  26. return self.messageType == kMessageTypeVoiceMessage
  27. }
  28. public var isCommandMessage: Bool {
  29. return self.messageType == kMessageTypeCommand
  30. }
  31. public func isMessage(from userId: String) -> Bool {
  32. return self.actorType == "users" && self.actorId == userId
  33. }
  34. public func isDeletable(for account: TalkAccount, in room: NCRoom) -> Bool {
  35. guard !self.isDeleting else { return false }
  36. let sixHoursAgoTimestamp = Int(Date().timeIntervalSince1970 - (6 * 3600))
  37. // Check server capability for normal messages
  38. var commentDeletion = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityDeleteMessages, forAccountId: account.accountId)
  39. commentDeletion = commentDeletion && self.messageType == kMessageTypeComment
  40. commentDeletion = commentDeletion && self.file() == nil
  41. commentDeletion = commentDeletion && !self.isObjectShare
  42. // Check server capability for files or shared objects
  43. var objectDeletion = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityRichObjectDelete)
  44. objectDeletion = objectDeletion && (self.file() != nil || self.isVoiceMessage || self.isObjectShare)
  45. // Check if user is allowed to delete a message
  46. let sameUser = self.isMessage(from: account.userId)
  47. let moderatorUser = (room.type != .oneToOne && room.type != .formerOneToOne) && (room.participantType == .owner || room.participantType == .moderator)
  48. let serverCanDeleteMessage = commentDeletion || objectDeletion
  49. let userCanDeleteMessage = sameUser || moderatorUser
  50. let noTimeLimitForMessageDeletion = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityDeleteMessagesUnlimited, forAccountId: account.accountId)
  51. let deletionAllowedByTime = noTimeLimitForMessageDeletion || (self.timestamp >= sixHoursAgoTimestamp)
  52. return serverCanDeleteMessage && userCanDeleteMessage && deletionAllowedByTime
  53. }
  54. public func isEditable(for account: TalkAccount, in room: NCRoom) -> Bool {
  55. guard !self.isDeleting else { return false }
  56. let twentyFourHoursAgoTimestamp = Int(Date().timeIntervalSince1970 - (24 * 3600))
  57. var serverCanEditMessage = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityEditMessages, forAccountId: account.accountId)
  58. serverCanEditMessage = serverCanEditMessage && self.messageType == kMessageTypeComment && !self.isObjectShare
  59. let sameUser = self.isMessage(from: account.userId)
  60. let moderatorUser = (room.type != .oneToOne && room.type != .formerOneToOne) && (room.participantType == .owner || room.participantType == .moderator)
  61. let userCanDeleteMessage = sameUser || moderatorUser
  62. let noTimeLimitForMessageEdit = (room.type == .noteToSelf) && NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityEditMessagesNoteToSelf, forAccountId: account.accountId)
  63. let editAllowedByTime = noTimeLimitForMessageEdit || (self.timestamp >= twentyFourHoursAgoTimestamp)
  64. return serverCanEditMessage && userCanDeleteMessage && editAllowedByTime
  65. }
  66. public var isObjectShare: Bool {
  67. return self.message != nil && self.message == "{object}" && self.messageParameters["object"] != nil
  68. }
  69. public var richObjectFromObjectShare: [AnyHashable: Any] {
  70. guard self.isObjectShare,
  71. let objectDict = self.messageParameters["object"] as? [AnyHashable: Any],
  72. let jsonData = try? JSONSerialization.data(withJSONObject: objectDict),
  73. let jsonString = String(data: jsonData, encoding: .utf8),
  74. let parameter = NCMessageParameter(dictionary: objectDict)
  75. else { return [:] }
  76. return [
  77. "objectType": parameter.type!,
  78. "objectId": parameter.parameterId!,
  79. "metaData": jsonString
  80. ]
  81. }
  82. public var poll: NCMessageParameter? {
  83. guard let objectParameter, objectParameter.type == "talk-poll"
  84. else { return nil }
  85. return objectParameter
  86. }
  87. public var objectParameter: NCMessageParameter? {
  88. guard self.isObjectShare,
  89. let objectDict = self.messageParameters["object"] as? [AnyHashable: Any],
  90. let objectParameter = NCMessageParameter(dictionary: objectDict)
  91. else { return nil }
  92. return objectParameter
  93. }
  94. public var messageParameters: [AnyHashable: Any] {
  95. guard let data = self.messageParametersJSONString?.data(using: .utf8),
  96. let dict = try? JSONSerialization.jsonObject(with: data) as? [AnyHashable: Any]
  97. else { return [:] }
  98. return dict
  99. }
  100. // TODO: Should probably be an optional?
  101. public var systemMessageFormat: NSMutableAttributedString {
  102. guard let message = self.parsedMessage() else { return NSMutableAttributedString(string: "") }
  103. return message.withTextColor(.tertiaryLabel)
  104. }
  105. // TODO: Should probably be an optional?
  106. public var sendingMessage: String {
  107. guard var resultMessage = self.message else { return "" }
  108. resultMessage = resultMessage.trimmingCharacters(in: .whitespacesAndNewlines)
  109. for case let (key as String, value as [AnyHashable: Any]) in self.messageParameters {
  110. if let parameter = NCMessageParameter(dictionary: value) {
  111. resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: parameter.mentionId)
  112. }
  113. }
  114. return resultMessage
  115. }
  116. public var parent: NCChatMessage? {
  117. guard !self.isDeletedMessage, self.parentId != nil else { return nil }
  118. var unmanagedChatMessage: NCChatMessage?
  119. if let managedChatMessage = NCChatMessage.objects(where: "internalId = %@", self.parentId).firstObject() {
  120. unmanagedChatMessage = NCChatMessage(value: managedChatMessage)
  121. }
  122. return unmanagedChatMessage
  123. }
  124. public var parentMessageId: Int {
  125. return self.parent?.messageId ?? -1
  126. }
  127. public func isReactionBeingModified(_ reaction: String) -> Bool {
  128. return self.temporaryReactions().first(where: { ($0 as? NCChatReaction)?.reaction == reaction }) != nil
  129. }
  130. public func removeReactionFromTemporaryReactions(_ reaction: String) {
  131. if let removeReaction = self.temporaryReactions().first(where: { ($0 as? NCChatReaction)?.reaction == reaction }) {
  132. self.temporaryReactions().remove(removeReaction)
  133. }
  134. }
  135. public func addTemporaryReaction(_ reaction: String) {
  136. let temporaryReaction = NCChatReaction()
  137. temporaryReaction.reaction = reaction
  138. temporaryReaction.state = .adding
  139. self.temporaryReactions().add(temporaryReaction)
  140. }
  141. public func removeReactionTemporarily(_ reaction: String) {
  142. let temporaryReaction = NCChatReaction()
  143. temporaryReaction.reaction = reaction
  144. temporaryReaction.state = .removing
  145. self.temporaryReactions().add(temporaryReaction)
  146. }
  147. internal var isReferenceApiSupported: Bool {
  148. // Check capabilities directly, otherwise NCSettingsController introduces new dependencies in NotificationServiceExtension
  149. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  150. if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) {
  151. return serverCapabilities.referenceApiSupported
  152. }
  153. return false
  154. }
  155. public func isSameMessage(_ message: NCChatMessage) -> Bool {
  156. if self.isTemporary {
  157. return self.referenceId == message.referenceId
  158. }
  159. return self.messageId == message.messageId
  160. }
  161. public var collapsedMessageParameters: [AnyHashable: Any] {
  162. guard let data = self.collapsedMessageParametersJSONString.data(using: .utf8),
  163. let dict = try? JSONSerialization.jsonObject(with: data) as? [AnyHashable: Any]
  164. else { return [:] }
  165. return dict
  166. }
  167. public func setCollapsedMessageParameters(_ messageParameters: [AnyHashable: Any]) {
  168. guard let jsonData = try? JSONSerialization.data(withJSONObject: messageParameters),
  169. let jsonString = String(data: jsonData, encoding: .utf8)
  170. else { return }
  171. self.collapsedMessageParametersJSONString = jsonString
  172. }
  173. public var actor: TalkActor {
  174. return TalkActor(actorId: self.actorId, actorType: self.actorType, actorDisplayName: self.actorDisplayName)
  175. }
  176. public var messageIconName: String? {
  177. if let file = self.file() {
  178. if NCUtils.isImage(fileType: file.mimetype) {
  179. return "photo"
  180. } else if NCUtils.isVideo(fileType: file.mimetype) {
  181. return "movieclapper"
  182. } else if NCUtils.isVCard(fileType: file.mimetype) {
  183. return "person.text.rectangle"
  184. } else if self.isVoiceMessage {
  185. return "mic"
  186. } else if NCUtils.isAudio(fileType: file.mimetype) {
  187. return "music.note"
  188. } else {
  189. return "doc"
  190. }
  191. } else if poll != nil {
  192. return "chart.bar"
  193. } else if deckCard() != nil {
  194. return "rectangle.stack"
  195. } else if geoLocation() != nil {
  196. return "location"
  197. }
  198. return nil
  199. }
  200. public var isAnimatableGif: Bool {
  201. guard let accountId, let file = self.file() else { return false }
  202. let capabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: accountId)
  203. guard NCUtils.isGif(fileType: file.mimetype), let maxGifSize = capabilities?.maxGifSize, maxGifSize > 0 else { return false }
  204. return file.size <= maxGifSize
  205. }
  206. }