mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 17:12:45 +02:00
b298dedc22
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
302 lines
13 KiB
Swift
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))
|
|
}
|
|
}
|