Files
bundesmessenger-ios/Riot/Modules/Pills/PillProvider.swift
T
JanNiklas Grabowski b298dedc22 chore: update from foss 1.11.19 (MESSENGER-6656)
Merge commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1' into feature/foss_update_1_11_19

* commit 'f823ab9aae70e8d15ed7cc079210dd9bbbb6c8e1':
  finish version++
  version++
  comments
  update submodule
  remove obsolete tests
  removed unused code
  update submodule
  fix
  Libolm removal
  update license macro
  update license
  Prepare for new sprint

# Conflicts:
#	Config/AppVersion.xcconfig
#	IDETemplateMacros.plist
#	LICENSE
#	README.md
#	Riot/Categories/MXSession+Riot.m
#	Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift
#	Riot/Managers/KeyValueStorage/Extensions/Keychain.swift
#	Riot/Managers/KeyValueStorage/KeyValueStore.swift
#	Riot/Managers/KeyValueStorage/KeychainStore.swift
#	Riot/Managers/KeyValueStorage/MemoryStore.swift
#	Riot/Managers/PushNotification/PushNotificationService.m
#	Riot/Managers/Settings/RiotSettings.swift
#	Riot/Managers/Settings/Shared/RiotSharedSettings.swift
#	Riot/Modules/Analytics/AnalyticsUIElement.swift
#	Riot/Modules/Application/AppCoordinator.swift
#	Riot/Modules/Application/LegacyAppDelegate.h
#	Riot/Modules/Application/LegacyAppDelegate.m
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.h
#	Riot/Modules/Authentication/Legacy/AuthenticationViewController.m
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.h
#	Riot/Modules/Authentication/Legacy/Views/AuthInputsView.m
#	Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
#	Riot/Modules/Common/Recents/RecentsViewController.m
#	Riot/Modules/Common/WebViewController/WebViewViewController.m
#	Riot/Modules/Contacts/Details/ContactDetailsViewController.m
#	Riot/Modules/Contacts/Views/ContactTableViewCell.m
#	Riot/Modules/Favorites/FavouritesViewController.h
#	Riot/Modules/Favorites/FavouritesViewController.m
#	Riot/Modules/GlobalSearch/UnifiedSearchViewController.m
#	Riot/Modules/People/PeopleViewController.h
#	Riot/Modules/People/PeopleViewController.m
#	Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift
#	Riot/Modules/Room/DataSources/RoomDataSource.m
#	Riot/Modules/Room/Files/RoomFilesViewController.m
#	Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
#	Riot/Modules/Room/Members/RoomParticipantsViewController.m
#	Riot/Modules/Room/RoomViewController.m
#	Riot/Modules/Room/Settings/RoomSettingsViewController.m
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift
#	Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroViewData.swift
#	Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
#	Riot/Modules/Rooms/RoomsViewController.h
#	Riot/Modules/Rooms/ShowDirectory/Cells/Network/DirectoryNetworkTableHeaderFooterView.swift
#	Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift
#	Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
#	Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
#	Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
#	Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
#	Riot/Modules/SetPinCode/PinCodePreferences.swift
#	Riot/Modules/SetPinCode/SetupBiometrics/BiometricsAuthenticationPresenter.swift
#	Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
#	Riot/Modules/Settings/Security/SecurityViewController.m
#	Riot/Modules/Settings/SettingsViewController.m
#	Riot/Modules/SplitView/SplitViewCoordinator.swift
#	Riot/Modules/SplitView/SplitViewCoordinatorType.swift
#	Riot/Modules/StartChat/StartChatViewController.m
#	Riot/Modules/TabBar/MasterTabBarController.h
#	Riot/Modules/TabBar/MasterTabBarController.m
#	Riot/Utils/EventFormatter.m
#	Riot/Utils/HTMLFormatter.swift
#	Riot/Utils/Tools.m
#	RiotNSE/NotificationService.swift
2024-10-18 15:45:54 +02:00

302 lines
13 KiB
Swift

//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
@available(iOS 15.0, *)
private enum PillAttachmentKind {
case attachment(PillTextAttachment)
case string(NSAttributedString)
}
@available(iOS 15.0, *)
struct PillProvider {
private let session: MXSession
private let eventFormatter: MXKEventFormatter
private let event: MXEvent?
private let roomState: MXRoomState
private let latestRoomState: MXRoomState?
private let isEditMode: Bool
init(withSession session: MXSession,
eventFormatter: MXKEventFormatter,
event: MXEvent?,
roomState: MXRoomState,
andLatestRoomState latestRoomState: MXRoomState?,
isEditMode: Bool) {
self.session = session
self.eventFormatter = eventFormatter
self.event = event
self.roomState = roomState
self.latestRoomState = latestRoomState
self.isEditMode = isEditMode
}
func pillTextAttachmentString(forUrl url: URL, withLabel label: String) -> NSAttributedString? {
// Try to get a pill from this url
guard let pillType = PillType.from(url: url) else {
return nil
}
// Do not pillify an url if it is a markdown or an http link (except for user and room) with a custom text
// First, we need to handle the case where the label can contains more than one # (room alias)
var urlFromLabel: URL?
/*
BWI: 6393 fix permalink to message not displayed as pill.
Due to changes in the latest iOS version, we need to remove the percent encoding from the label before searching for markdown or an http link.
The URL extracted from an attributed string now returns a URL without percent encoding. Checking against the encoded label will always fail.
*/
if #available(iOS 18.0, *) {
urlFromLabel = URL(string: label.removingPercentEncoding ?? label)?.absoluteURL
} else {
urlFromLabel = URL(string: label)?.absoluteURL
}
if urlFromLabel == nil, label.filter({ $0 == "#" }).count > 1 {
if let escapedLabel = label.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedLabel) {
urlFromLabel = Tools.fixURL(withSeveralHashKeys: url)
}
}
let fixedUrl = Tools.fixURL(withSeveralHashKeys: url)
let isUrlMarkDownLink = urlFromLabel != fixedUrl
let result: PillAttachmentKind
switch pillType {
case .user(let userId):
var userFound = false
result = pillTextAttachment(forUserId: userId, userFound: &userFound)
// if it is a markdown link and we didn't found the user, don't pillify it
if isUrlMarkDownLink && !userFound {
return nil
}
case .room(let roomId):
var roomFound = false
result = pillTextAttachment(forRoomId: roomId, roomFound: &roomFound)
// if it is a markdown link and we didn't found the room, don't pillify it
if isUrlMarkDownLink && !roomFound {
return nil
}
case .message(let roomId, let messageId):
// if it is a markdown link, don't pillify it
if isUrlMarkDownLink {
return nil
}
result = pillTextAttachment(forMessageId: messageId, inRoomId: roomId)
}
switch result {
case .attachment(let pillTextAttachment):
return PillsFormatter.attributedStringWithAttachment(pillTextAttachment, link: isEditMode ? nil : url, font: eventFormatter.defaultTextFont)
case .string(let attributedString):
// if we don't have an attachment, use the fallback attributed string
let newAttrString = NSMutableAttributedString(attributedString: attributedString)
if let font = eventFormatter.defaultTextFont {
newAttrString.addAttribute(.font, value: font, range: .init(location: 0, length: newAttrString.length))
}
newAttrString.addAttribute(.foregroundColor, value: ThemeService.shared().theme.colors.links, range: .init(location: 0, length: newAttrString.length))
newAttrString.addAttribute(.link, value: url, range: .init(location: 0, length: newAttrString.length))
return newAttrString
}
}
/// Retrieve the latest available `MXRoomMember` from given data.
///
/// - Parameters:
/// - userId: the id of the user
/// - Returns: the room member, if available
private func roomMember(withUserId userId: String) -> MXRoomMember? {
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
}
/// Create a pill representation for a given user
/// - Parameters:
/// - userId: the user MatrixID
/// - userFound: this flag will be set to true if a user is found locally with this userId
/// - Returns: a pill attachment
private func pillTextAttachment(forUserId userId: String, userFound: inout Bool) -> PillAttachmentKind {
// Search for a room member matching this user id
let roomMember = self.roomMember(withUserId: userId)
var user: MXUser?
if roomMember == nil {
// fallback on getting the user from the session's store
user = session.user(withUserId: userId)
}
let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl
let displayName = roomMember?.displayname ?? user?.displayName ?? userId
let isHighlighted = userId == session.myUserId
// No actual event means it is a composer Pill. No highlight
&& event != nil
// No highlight on self-mentions
&& event?.sender != session.myUserId
let avatar: PillTextAttachmentItem
if roomMember == nil && user == nil {
avatar = .asset(named: "pill_user",
parameters: .init(tintColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.secondaryContent),
rawRenderingMode: UIImage.RenderingMode.alwaysOriginal.rawValue,
padding: 0.0))
} else {
avatar = .avatar(url: avatarUrl,
string: displayName,
matrixId: userId)
}
let data = PillTextAttachmentData(pillType: .user(userId: userId),
items: [
avatar,
.text(displayName)
],
isHighlighted: isHighlighted,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
userFound = roomMember != nil || user != nil
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayName))
}
/// Create a pill representation for a given room
/// - Parameters:
/// - roomId: the room MXID or alias
/// - roomFound: this flag will be set to true if a room is found locally with this roomId
/// - Returns: a pill attachment
private func pillTextAttachment(forRoomId roomId: String, roomFound: inout Bool) -> PillAttachmentKind {
// Get the room matching this roomId
let room = roomId.starts(with: "#") ? session.room(withAlias: roomId) : session.room(withRoomId: roomId)
let displayName = room?.displayName ?? VectorL10n.pillRoomFallbackDisplayName
let avatar: PillTextAttachmentItem
if let room {
if session.spaceService.getSpace(withId: roomId) != nil {
avatar = .spaceAvatar(url: room.avatarData.mxContentUri,
string: displayName,
matrixId: roomId)
} else {
avatar = .avatar(url: room.avatarData.mxContentUri,
string: displayName,
matrixId: roomId)
}
} else {
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let data = PillTextAttachmentData(pillType: .room(roomId: roomId),
items: [
avatar,
.text(displayName)
],
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
roomFound = room != nil
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayName))
}
/// Create a pill representation for a message in a room
/// - Parameters:
/// - messageId: message eventId
/// - roomId: roomId of the message
/// - Returns: a pill attachment
private func pillTextAttachment(forMessageId messageId: String, inRoomId roomId: String) -> PillAttachmentKind {
// Check if this is the current room
if roomId == roomState.roomId {
return pillTextAttachment(inCurrentRoomForMessageId: messageId)
}
let room = session.room(withRoomId: roomId)
let avatar: PillTextAttachmentItem
if let room {
avatar = .avatar(url: room.avatarData.mxContentUri,
string: room.displayName,
matrixId: roomId)
} else {
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let displayText = room?.displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage
let data = PillTextAttachmentData(pillType: .message(roomId: roomId, eventId: messageId),
items: [
avatar,
.text(displayText)
],
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayText))
}
/// Create a pill representation for a message in the current room
/// - Parameters:
/// - messageId: message eventId
/// - Returns: a pill attachment
private func pillTextAttachment(inCurrentRoomForMessageId messageId: String) -> PillAttachmentKind {
var roomMember: MXRoomMember?
// If we have the event locally, try to get the room member
if let event = session.store.event(withEventId: messageId, inRoom: roomState.roomId) {
roomMember = self.roomMember(withUserId: event.sender)
}
let displayText: String
let avatar: PillTextAttachmentItem
if let roomMember {
displayText = VectorL10n.pillMessageFrom(roomMember.displayname ?? roomMember.userId)
avatar = .avatar(url: roomMember.avatarUrl,
string: roomMember.displayname,
matrixId: roomMember.userId)
} else {
displayText = VectorL10n.pillMessage
avatar = .asset(named: "link_icon",
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
}
let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId),
items: [
avatar,
.text(displayText)
].compactMap { $0 },
isHighlighted: false,
alpha: 1.0,
font: eventFormatter.defaultTextFont)
if let attachment = PillTextAttachment(attachmentData: data) {
return .attachment(attachment)
}
return .string(NSMutableAttributedString(string: displayText))
}
}