Merge pull request #7549 from vector-im/release/1.10.12/release

Release 1.10.12
This commit is contained in:
Stefan Ceriu 2023-05-16 16:24:23 +03:00 committed by GitHub
commit 3c478e0581
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 2633 additions and 2388 deletions

View file

@ -53,23 +53,10 @@ jobs:
contains(github.event.issue.labels.*.name, 'O-Frequent')) || contains(github.event.issue.labels.*.name, 'O-Frequent')) ||
contains(github.event.issue.labels.*.name, 'A11y')) contains(github.event.issue.labels.*.name, 'A11y'))
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/18
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc0sUA"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
add_product_issues_to_project: add_product_issues_to_project:
name: X-Needs-Product to Design project board name: X-Needs-Product to Design project board
@ -77,138 +64,10 @@ jobs:
if: > if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Product') contains(github.event.issue.labels.*.name, 'X-Needs-Product')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/28
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AAg6N"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
Delight_issues_to_board:
name: Spaces issues to Delight project board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'Team: Delight') ||
contains(github.event.issue.labels.*.name, 'Z-AppLayout')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc1HvQ"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_voice-message_issues:
name: A-Voice Messages to voice message board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc2KCw"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_message_bubble_issues:
name: A-Message-Bubbles to Message bubble board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc3m-g"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_FTUE_issues:
name: Z-FTUE to FTUE board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'Z-FTUE')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AAqVx"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_WTF_issues:
name: Z-WTF to WTF board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'Z-WTF')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AArk0"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ex_plorers: ex_plorers:
name: Add labelled issues to X-Plorer project name: Add labelled issues to X-Plorer project
@ -216,23 +75,10 @@ jobs:
if: > if: >
contains(github.event.issue.labels.*.name, 'Team: Element X Feature') contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/73
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4ALoFY"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features1: ps_features1:
name: Add labelled issues to PS features team 1 name: Add labelled issues to PS features team 1
@ -245,23 +91,10 @@ jobs:
(contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') && (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') &&
contains(github.event.issue.labels.*.name, 'A-User-Settings')) contains(github.event.issue.labels.*.name, 'A-User-Settings'))
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/56
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AHJKF"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features2: ps_features2:
name: Add labelled issues to PS features team 2 name: Add labelled issues to PS features team 2
@ -270,23 +103,10 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-DM-Start') || contains(github.event.issue.labels.*.name, 'A-DM-Start') ||
contains(github.event.issue.labels.*.name, 'A-Broadcast') contains(github.event.issue.labels.*.name, 'A-Broadcast')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/58
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AHJKd"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features3: ps_features3:
name: Add labelled issues to PS features team 3 name: Add labelled issues to PS features team 3
@ -294,23 +114,10 @@ jobs:
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/57
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
voip: voip:
name: Add labelled issues to VoIP project board name: Add labelled issues to VoIP project board
@ -318,20 +125,7 @@ jobs:
if: > if: >
contains(github.event.issue.labels.*.name, 'Team: VoIP') contains(github.event.issue.labels.*.name, 'Team: VoIP')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: actions/add-to-project@main
id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' project-url: https://github.com/orgs/vector-im/projects/41
query: | github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4ABMIk"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,3 +1,34 @@
## Changes in 1.10.12 (2023-05-16)
✨ Features
- Add composer suggestions for slash commands ([#7493](https://github.com/vector-im/element-ios/issues/7493))
🙌 Improvements
- Crypto: Deprecate MXLegacyCrypto ([#7508](https://github.com/vector-im/element-ios/pull/7508))
- Add a flag in the build settings to force the user to define a homeserver instead of using the default one. ([#7541](https://github.com/vector-im/element-ios/pull/7541))
- Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)).
- Add an audio alert when the voice broadcast recording is automatically paused ([#7504](https://github.com/vector-im/element-ios/issues/7504))
- Timeline: Remove the matrix ID displayed when someone has changed its display name. ([#7517](https://github.com/vector-im/element-ios/issues/7517))
🐛 Bugfixes
- Fix an issue where the Secrets Reset screen would open twice. ([#7404](https://github.com/vector-im/element-ios/pull/7404))
- Make sure to use the chosen language for the VoiceOver voice too. ([#7493](https://github.com/vector-im/element-ios/pull/7493))
- Fix the position of the send confirmation icon. ([#7512](https://github.com/vector-im/element-ios/pull/7512))
- Disable accessibility for emojis during session verification. ([#7521](https://github.com/vector-im/element-ios/pull/7521))
- Fix accessibility when entering the PIN to unlock the app. ([#7522](https://github.com/vector-im/element-ios/pull/7522))
- Fix voiceover order of room creation header and message composer. ([#7543](https://github.com/vector-im/element-ios/pull/7543))
- Fix: The last event description text color now matches the active theme. ([#7545](https://github.com/vector-im/element-ios/pull/7545))
- Fix mention pills display in thread list ([#7322](https://github.com/vector-im/element-ios/issues/7322))
- Poll: The timeline sometimes displayed closed polls in the wrong order. ([#7497](https://github.com/vector-im/element-ios/issues/7497))
- Fix a flickering issue when the timeline datasource is reloaded. ([#7523](https://github.com/vector-im/element-ios/issues/7523))
- Fix the position of the marker highlighting an event. ([#7526](https://github.com/vector-im/element-ios/issues/7526))
- Fix application crashing when opening a thread with RTE enabled ([#7530](https://github.com/vector-im/element-ios/issues/7530))
- Labs: Rich Text Editor: Fix partial text messages not being saved for each room ([#7535](https://github.com/vector-im/element-ios/issues/7535))
## Changes in 1.10.11 (2023-04-18) ## Changes in 1.10.11 (2023-04-18)
🙌 Improvements 🙌 Improvements

View file

@ -15,5 +15,5 @@
// //
// Version // Version
MARKETING_VERSION = 1.10.11 MARKETING_VERSION = 1.10.12
CURRENT_PROJECT_VERSION = 1.10.11 CURRENT_PROJECT_VERSION = 1.10.12

View file

@ -98,8 +98,13 @@ final class BuildSettings: NSObject {
// MARK: - Server configuration // MARK: - Server configuration
// Default servers proposed on the authentication screen /// Force the user to set a homeserver instead of using the default one
static let forceHomeserverSelection = false
/// Default server proposed on the authentication screen
static let serverConfigDefaultHomeserverUrlString = "https://matrix.org" static let serverConfigDefaultHomeserverUrlString = "https://matrix.org"
/// Default identity server
static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" static let serverConfigDefaultIdentityServerUrlString = "https://vector.im"
static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify" static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify"

View file

@ -92,8 +92,7 @@ class CommonConfiguration: NSObject, Configurable {
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
// Configure Crypto SDK feature deciding which crypto module to use sdkOptions.cryptoMigrationDelegate = self
sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared
} }
private func makeASCIIUserAgent() -> String? { private func makeASCIIUserAgent() -> String? {
@ -169,13 +168,15 @@ class CommonConfiguration: NSObject, Configurable {
callManager.fallbackSTUNServer = stunServerFallback callManager.fallbackSTUNServer = stunServerFallback
} }
} }
}
// MARK: - Per loaded matrix session settings extension CommonConfiguration: MXCryptoV2MigrationDelegate {
var needsVerificationUpgrade: Bool {
func setupSettingsWhenLoaded(for matrixSession: MXSession) { get {
// Do not warn for unknown devices. We have cross-signing now RiotSettings.shared.showVerificationUpgradeAlert
(matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false }
} set {
RiotSettings.shared.showVerificationUpgradeAlert = newValue
}
}
} }

View file

@ -24,7 +24,4 @@ import MatrixSDK
// MARK: - Per matrix session settings // MARK: - Per matrix session settings
func setupSettings(for matrixSession: MXSession) func setupSettings(for matrixSession: MXSession)
// MARK: - Per loaded matrix session settings
func setupSettingsWhenLoaded(for matrixSession: MXSession)
} }

View file

@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
# #
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
$matrixSDKVersion = '= 0.26.9' $matrixSDKVersion = '= 0.26.10'
# $matrixSDKVersion = :local # $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }

View file

@ -50,8 +50,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : { "state" : {
"revision" : "758f226a92d6726ab626c1e78ecd183bdba77016", "revision" : "ff5e8054da60212051cb0dec244500ca0f441bac",
"version" : "2.0.0" "version" : "2.1.0"
} }
}, },
{ {

Binary file not shown.

View file

@ -614,6 +614,21 @@ Tap the + to start adding people.";
"room_join_group_call" = "Join"; "room_join_group_call" = "Join";
"room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; "room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call.";
// Room commands descriptions
"room_command_change_display_name_description" = "Changes your display nickname";
"room_command_emote_description" = "Displays action";
"room_command_join_room_description" = "Joins room with given address";
"room_command_part_room_description" = "Leave room";
"room_command_invite_user_description" = "Invites user with given id to current room";
"room_command_kick_user_description" = "Removes user with given id from this room";
"room_command_ban_user_description" = "Bans user with given id";
"room_command_unban_user_description" = "Unbans user with given id";
"room_command_set_user_power_level_description" = "Define the power level of a user";
"room_command_reset_user_power_level_description" = "Deops user with given id";
"room_command_change_room_topic_description" = "Sets the room topic";
"room_command_discard_session_description" = "Forces the current outbound group session in an encrypted room to be discarded";
"room_command_error_unknown_command" = "Invalid or unhandled command";
// MARK: Threads // MARK: Threads
"room_thread_title" = "Thread"; "room_thread_title" = "Thread";
"thread_copy_link_to_thread" = "Copy link to thread"; "thread_copy_link_to_thread" = "Copy link to thread";
@ -807,9 +822,6 @@ Tap the + to start adding people.";
"settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_new_app_layout" = "New Application Layout";
"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor";
"settings_labs_enable_voice_broadcast" = "Voice broadcast"; "settings_labs_enable_voice_broadcast" = "Voice broadcast";
"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption";
"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution.";
"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)";
"settings_version" = "Version %@"; "settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@"; "settings_olm_version" = "Olm Version %@";
@ -2393,6 +2405,8 @@ Tap the + to start adding people.";
"poll_timeline_reply_ended_poll" = "Ended poll"; "poll_timeline_reply_ended_poll" = "Ended poll";
"poll_timeline_loading" = "Loading...";
// MARK: - Location sharing // MARK: - Location sharing
"location_sharing_title" = "Location"; "location_sharing_title" = "Location";
@ -2972,6 +2986,7 @@ To enable access, tap Settings> Location and select Always";
"notice_avatar_url_changed" = "%@ changed their avatar"; "notice_avatar_url_changed" = "%@ changed their avatar";
"notice_display_name_set" = "%@ set their display name to %@"; "notice_display_name_set" = "%@ set their display name to %@";
"notice_display_name_changed_from" = "%@ changed their display name from %@ to %@"; "notice_display_name_changed_from" = "%@ changed their display name from %@ to %@";
"notice_display_name_changed_to" = "%@ changed their display name to %@";
"notice_display_name_removed" = "%@ removed their display name"; "notice_display_name_removed" = "%@ removed their display name";
"notice_topic_changed" = "%@ changed the topic to \"%@\"."; "notice_topic_changed" = "%@ changed the topic to \"%@\".";
"notice_room_name_changed" = "%@ changed the room name to %@."; "notice_room_name_changed" = "%@ changed the room name to %@.";

View file

@ -70,7 +70,6 @@ extension MXBugReportRestClient {
// SDKs // SDKs
userInfo["matrix_sdk_version"] = MatrixSDKVersion userInfo["matrix_sdk_version"] = MatrixSDKVersion
userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId
if let crypto = mainAccount?.mxSession?.crypto { if let crypto = mainAccount?.mxSession?.crypto {
userInfo["crypto_module_version"] = crypto.version userInfo["crypto_module_version"] = crypto.version
} }

View file

@ -256,40 +256,18 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
if (componentIndex < bubbleComponents.count) if (componentIndex < bubbleComponents.count)
{ {
MXKRoomBubbleComponent *component = bubbleComponents[componentIndex]; CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex];
if (CGRectIsEmpty(componentFrame))
// Define the marker frame
CGFloat markPosY = component.position.y + self.msgTextViewTopConstraint.constant;
NSInteger mostRecentComponentIndex = bubbleComponents.count - 1;
if ([bubbleData isKindOfClass:RoomBubbleCellData.class])
{ {
mostRecentComponentIndex = ((RoomBubbleCellData*)bubbleData).mostRecentComponentIndex; return;
} }
// Compute the mark height. CGRect markerFrame = CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X,
// Use the rest of the cell height by default. CGRectGetMinY(componentFrame),
CGFloat markHeight = self.contentView.frame.size.height - markPosY;
if (componentIndex != mostRecentComponentIndex)
{
// There is another component (with display) after this component in the cell.
// Stop the marker height to the top of this component.
for (NSInteger index = componentIndex + 1; index < bubbleComponents.count; index ++)
{
MXKRoomBubbleComponent *nextComponent = bubbleComponents[index];
if (nextComponent.attributedTextMessage)
{
markHeight = nextComponent.position.y - component.position.y;
break;
}
}
}
UIView *markerView = [[UIView alloc] initWithFrame:CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X,
markPosY,
VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH, VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH,
markHeight)]; CGRectGetHeight(componentFrame));
UIView *markerView = [[UIView alloc] initWithFrame:markerFrame];
markerView.backgroundColor = ThemeService.shared.theme.tintColor; markerView.backgroundColor = ThemeService.shared.theme.tintColor;
[markerView setTranslatesAutoresizingMaskIntoConstraints:NO]; [markerView setTranslatesAutoresizingMaskIntoConstraints:NO];
@ -303,28 +281,28 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
toItem:self.contentView toItem:self.contentView
attribute:NSLayoutAttributeLeading attribute:NSLayoutAttributeLeading
multiplier:1.0 multiplier:1.0
constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X]; constant:CGRectGetMinX(markerFrame)];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeTop attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual relatedBy:NSLayoutRelationEqual
toItem:self.contentView toItem:self.contentView
attribute:NSLayoutAttributeTop attribute:NSLayoutAttributeTop
multiplier:1.0 multiplier:1.0
constant:markPosY]; constant:CGRectGetMinY(markerFrame)];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeWidth attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual relatedBy:NSLayoutRelationEqual
toItem:nil toItem:nil
attribute:NSLayoutAttributeNotAnAttribute attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0 multiplier:1.0
constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH]; constant:CGRectGetWidth(markerFrame)];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeHeight attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual relatedBy:NSLayoutRelationEqual
toItem:nil toItem:nil
attribute:NSLayoutAttributeNotAnAttribute attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0 multiplier:1.0
constant:markHeight]; constant:CGRectGetHeight(markerFrame)];
// Available on iOS 8 and later // Available on iOS 8 and later
[NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]]; [NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]];
@ -600,19 +578,29 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
} }
else if (roomBubbleTableViewCell.messageTextView) else if (roomBubbleTableViewCell.messageTextView)
{ {
CGFloat textMessageHeight = 0; // Force the textView used underneath to layout its frame properly
[roomBubbleTableViewCell setNeedsLayout];
[roomBubbleTableViewCell layoutIfNeeded];
// Compute the height
CGFloat textMessageHeight = 0;
if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]]) if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]])
{ {
RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData; RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData;
if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage) if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage)
{ {
textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage]; // Get the width of messageTextView to compute the needed height
CGFloat maxTextWidth = CGRectGetWidth(roomBubbleTableViewCell.messageTextView.bounds);
// Compute text message height
textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage withMaxWidth:maxTextWidth];
} }
} }
selectedComponentPositionY = selectedComponent.position.y; // Get the messageText frame in the cell content view (as the messageTextView may be inside a stackView and not directly a child of the tableViewCell)
UITextView *messageTextView = roomBubbleTableViewCell.messageTextView;
CGRect messageTextViewFrame = [messageTextView convertRect:messageTextView.bounds toView:roomBubbleTableViewCell.contentView];
if (textMessageHeight > 0) if (textMessageHeight > 0)
{ {
@ -620,14 +608,15 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
} }
else else
{ {
selectedComponentHeight = roomBubbleTableViewCell.frame.size.height - selectedComponentPositionY; // if we don't have a height, use the messageTextView height without the text container vertical insets to stay aligned with the text.
selectedComponentHeight = CGRectGetHeight(messageTextViewFrame) - messageTextView.textContainerInset.top - messageTextView.textContainerInset.bottom;
} }
// Force the textView used underneath to layout its frame properly // Get the vertical position of the messageTextView relative to the contentView
[roomBubbleTableViewCell setNeedsLayout]; selectedComponenContentViewYOffset = CGRectGetMinY(messageTextViewFrame);
[roomBubbleTableViewCell layoutIfNeeded];
selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y; // Get the position of the component inside the messageTextView
selectedComponentPositionY = selectedComponent.position.y;
} }
if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView) if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView)
@ -801,8 +790,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
- (void)addTickView:(UIView *)tickView atIndex:(NSInteger)index - (void)addTickView:(UIView *)tickView atIndex:(NSInteger)index
{ {
CGRect componentFrame = [self componentFrameInContentViewForIndex: index]; CGRect componentFrame = [self componentFrameInContentViewForIndex:index];
tickView.frame = CGRectMake(self.contentView.bounds.size.width - tickView.frame.size.width - 2 * PlainRoomCellLayoutConstants.readReceiptsViewRightMargin, CGRectGetMaxY(componentFrame) - tickView.frame.size.height, tickView.frame.size.width, tickView.frame.size.height); tickView.frame = CGRectMake(self.contentView.bounds.size.width - tickView.frame.size.width - 2 * PlainRoomCellLayoutConstants.readReceiptsViewRightMargin, CGRectGetMaxY(componentFrame) - tickView.frame.size.height, tickView.frame.size.width, tickView.frame.size.height);
[self.contentView addSubview:tickView]; [self.contentView addSubview:tickView];

View file

@ -20,7 +20,7 @@
#import "AvatarGenerator.h" #import "AvatarGenerator.h"
#import "MatrixKit.h" #import "MatrixKit.h"
#import "GeneratedInterface-Swift.h"
#import <objc/runtime.h> #import <objc/runtime.h>
@implementation MXRoom (Riot) @implementation MXRoom (Riot)
@ -331,30 +331,10 @@
{ {
[self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) { [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) {
UserEncryptionTrustLevel userEncryptionTrustLevel; MXCrossSigningInfo *crossSigningInfo = [self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId];
double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted; EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
UserEncryptionTrustLevel userEncryptionTrustLevel = [encryption userTrustLevelWithCrossSigning:crossSigningInfo
if (trustedDevicesPercentage >= 1.0) trustedDevicesProgress:usersTrustLevelSummary.trustedDevicesProgress];
{
userEncryptionTrustLevel = UserEncryptionTrustLevelTrusted;
}
else if (trustedDevicesPercentage == 0.0)
{
// Verify if the user has the user has cross-signing enabled
if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId])
{
userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified;
}
else
{
userEncryptionTrustLevel = UserEncryptionTrustLevelNoCrossSigning;
}
}
else
{
userEncryptionTrustLevel = UserEncryptionTrustLevelWarning;
}
onComplete(userEncryptionTrustLevel); onComplete(userEncryptionTrustLevel);
} failure:^(NSError *error) { } failure:^(NSError *error) {

View file

@ -15,17 +15,7 @@
*/ */
#import "MatrixKit.h" #import "MatrixKit.h"
#import "RoomEncryptionTrustLevel.h"
/**
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
*/
typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
RoomEncryptionTrustLevelTrusted,
RoomEncryptionTrustLevelWarning,
RoomEncryptionTrustLevelNormal,
RoomEncryptionTrustLevelUnknown
};
/** /**
Define a `MXRoomSummary` category at Riot level. Define a `MXRoomSummary` category at Riot level.

View file

@ -33,32 +33,15 @@
- (RoomEncryptionTrustLevel)roomEncryptionTrustLevel - (RoomEncryptionTrustLevel)roomEncryptionTrustLevel
{ {
RoomEncryptionTrustLevel roomEncryptionTrustLevel = RoomEncryptionTrustLevelUnknown; MXUsersTrustLevelSummary *trust = self.trust;
if (self.trust) if (!trust)
{ {
double trustedUsersPercentage = self.trust.trustedUsersProgress.fractionCompleted; MXLogError(@"[MXRoomSummary] roomEncryptionTrustLevel: trust is missing");
double trustedDevicesPercentage = self.trust.trustedDevicesProgress.fractionCompleted; return RoomEncryptionTrustLevelUnknown;
if (trustedUsersPercentage >= 1.0)
{
if (trustedDevicesPercentage >= 1.0)
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelTrusted;
}
else
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelWarning;
}
}
else
{
roomEncryptionTrustLevel = RoomEncryptionTrustLevelNormal;
} }
roomEncryptionTrustLevel = roomEncryptionTrustLevel; EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
} return [encryption roomTrustLevelWithSummary:trust];
return roomEncryptionTrustLevel;
} }
- (BOOL)isJoined - (BOOL)isJoined

View file

@ -0,0 +1,64 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Custom NSAttributedString.Key to specify the theme
let themeIdentifierAttributeName = NSAttributedString.Key("ThemeIdentifier")
/// Custom NSAttributedString.Key to specify a theme color by its name
let themeColorNameAttributeName = NSAttributedString.Key("ThemeColorName")
extension NSAttributedString {
/// Fix foreground color attributes if this attributed string contains the `themeIdentifierAttributeName` and `foregroundColorNameAttributeName` attributes
/// - Returns: a new attributed string with updated colors
@objc func fixForegroundColor() -> NSAttributedString {
let activeTheme = ThemeService.shared().theme
// Check if a theme is defined for this attributed string
var needUpdate = false
self.vc_enumerateAttribute(themeIdentifierAttributeName) { (themeIdentifier: String, range: NSRange, _) in
needUpdate = themeIdentifier != activeTheme.identifier
}
guard needUpdate else {
return self
}
// Build a new attributedString with the proper colors if possible
let mutableAttributedString = NSMutableAttributedString(attributedString: self)
mutableAttributedString.vc_enumerateAttribute(themeColorNameAttributeName) { (colorName: String, range: NSRange, _) in
if let color = ThemeColorResolver.getColorByName(colorName) {
mutableAttributedString.addAttribute(.foregroundColor, value: color, range: range)
}
}
return mutableAttributedString
}
}
extension NSMutableAttributedString {
/// Adds a theme color name attribute
/// - Parameters:
/// - colorName: color name
/// - range:range for this attribute
@objc func addThemeColorNameAttribute(_ colorName: String, range: NSRange) {
self.addAttribute(themeColorNameAttributeName, value: colorName, range: range)
}
/// Adds a theme identifier attribute
@objc func addThemeIdentifierAttribute() {
self.addAttribute(themeIdentifierAttributeName, value: ThemeService.shared().theme.identifier, range: .init(location: 0, length: length))
}
}

View file

@ -1,116 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixSDKCrypto
/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status
/// of `CryptoSDK`, and which uses feature flags to control rollout availability.
///
/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`.
/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases,
/// it is not available to all users because it requires data tracking user consent. Remote therefore
/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually
/// targetting all users, but each target change requires new app release.
///
/// Additionally users can manually enable this feature from the settings if they are not already in the
/// feature group.
@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature {
@objc static let shared = CryptoSDKFeature()
var isEnabled: Bool {
RiotSettings.shared.enableCryptoSDK
}
var needsVerificationUpgrade: Bool {
get {
return RiotSettings.shared.showVerificationUpgradeAlert
}
set {
RiotSettings.shared.showVerificationUpgradeAlert = newValue
}
}
private static let FeatureName = "ios-crypto-sdk"
private static let FeatureNameV2 = "ios-crypto-sdk-v2"
private let remoteFeature: RemoteFeaturesClientProtocol
private let localFeature: PhasedRolloutFeature
init(
remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared,
localTargetPercentage: Double = 1
) {
self.remoteFeature = remoteFeature
self.localFeature = PhasedRolloutFeature(
name: Self.FeatureName,
targetPercentage: localTargetPercentage
)
}
func enable() {
RiotSettings.shared.enableCryptoSDK = true
Analytics.shared.trackCryptoSDKEnabled()
MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled")
}
func enableIfAvailable(forUserId userId: String!) {
guard !isEnabled else {
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled")
return
}
guard let userId else {
MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id")
return
}
guard isFeatureEnabled(userId: userId) else {
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user")
return
}
MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled")
enable()
}
@objc func canManuallyEnable(forUserId userId: String!) -> Bool {
guard let userId else {
MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id")
return false
}
// User can manually enable only if not already within the automatic feature group
return !isFeatureEnabled(userId: userId)
}
@objc func reset() {
RiotSettings.shared.enableCryptoSDK = false
MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled")
}
private func isFeatureEnabled(userId: String) -> Bool {
// This feature includes app version with a bug, and thus will not be rolled out to 100% users
remoteFeature.isFeatureEnabled(Self.FeatureName)
// Second version of the remote feature with a bugfix and released eventually to 100% users
|| remoteFeature.isFeatureEnabled(Self.FeatureNameV2)
// Local feature
|| localFeature.isEnabled(userId: userId)
}
}

View file

@ -3899,6 +3899,10 @@ public class VectorL10n: NSObject {
public static func noticeDisplayNameChangedFromByYou(_ p1: String, _ p2: String) -> String { public static func noticeDisplayNameChangedFromByYou(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "notice_display_name_changed_from_by_you", p1, p2) return VectorL10n.tr("Vector", "notice_display_name_changed_from_by_you", p1, p2)
} }
/// %@ changed their display name to %@
public static func noticeDisplayNameChangedTo(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "notice_display_name_changed_to", p1, p2)
}
/// %@ removed their display name /// %@ removed their display name
public static func noticeDisplayNameRemoved(_ p1: String) -> String { public static func noticeDisplayNameRemoved(_ p1: String) -> String {
return VectorL10n.tr("Vector", "notice_display_name_removed", p1) return VectorL10n.tr("Vector", "notice_display_name_removed", p1)
@ -4923,6 +4927,10 @@ public class VectorL10n: NSObject {
public static var pollTimelineEndedText: String { public static var pollTimelineEndedText: String {
return VectorL10n.tr("Vector", "poll_timeline_ended_text") return VectorL10n.tr("Vector", "poll_timeline_ended_text")
} }
/// Loading...
public static var pollTimelineLoading: String {
return VectorL10n.tr("Vector", "poll_timeline_loading")
}
/// Please try again /// Please try again
public static var pollTimelineNotClosedSubtitle: String { public static var pollTimelineNotClosedSubtitle: String {
return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle")
@ -5211,6 +5219,58 @@ public class VectorL10n: NSObject {
public static var roomAvatarViewAccessibilityLabel: String { public static var roomAvatarViewAccessibilityLabel: String {
return VectorL10n.tr("Vector", "room_avatar_view_accessibility_label") return VectorL10n.tr("Vector", "room_avatar_view_accessibility_label")
} }
/// Bans user with given id
public static var roomCommandBanUserDescription: String {
return VectorL10n.tr("Vector", "room_command_ban_user_description")
}
/// Changes your display nickname
public static var roomCommandChangeDisplayNameDescription: String {
return VectorL10n.tr("Vector", "room_command_change_display_name_description")
}
/// Sets the room topic
public static var roomCommandChangeRoomTopicDescription: String {
return VectorL10n.tr("Vector", "room_command_change_room_topic_description")
}
/// Forces the current outbound group session in an encrypted room to be discarded
public static var roomCommandDiscardSessionDescription: String {
return VectorL10n.tr("Vector", "room_command_discard_session_description")
}
/// Displays action
public static var roomCommandEmoteDescription: String {
return VectorL10n.tr("Vector", "room_command_emote_description")
}
/// Invalid or unhandled command
public static var roomCommandErrorUnknownCommand: String {
return VectorL10n.tr("Vector", "room_command_error_unknown_command")
}
/// Invites user with given id to current room
public static var roomCommandInviteUserDescription: String {
return VectorL10n.tr("Vector", "room_command_invite_user_description")
}
/// Joins room with given address
public static var roomCommandJoinRoomDescription: String {
return VectorL10n.tr("Vector", "room_command_join_room_description")
}
/// Removes user with given id from this room
public static var roomCommandKickUserDescription: String {
return VectorL10n.tr("Vector", "room_command_kick_user_description")
}
/// Leave room
public static var roomCommandPartRoomDescription: String {
return VectorL10n.tr("Vector", "room_command_part_room_description")
}
/// Deops user with given id
public static var roomCommandResetUserPowerLevelDescription: String {
return VectorL10n.tr("Vector", "room_command_reset_user_power_level_description")
}
/// Define the power level of a user
public static var roomCommandSetUserPowerLevelDescription: String {
return VectorL10n.tr("Vector", "room_command_set_user_power_level_description")
}
/// Unbans user with given id
public static var roomCommandUnbanUserDescription: String {
return VectorL10n.tr("Vector", "room_command_unban_user_description")
}
/// You need permission to manage conference call in this room /// You need permission to manage conference call in this room
public static var roomConferenceCallNoPower: String { public static var roomConferenceCallNoPower: String {
return VectorL10n.tr("Vector", "room_conference_call_no_power") return VectorL10n.tr("Vector", "room_conference_call_no_power")
@ -7647,18 +7707,10 @@ public class VectorL10n: NSObject {
public static var settingsLabs: String { public static var settingsLabs: String {
return VectorL10n.tr("Vector", "settings_labs") return VectorL10n.tr("Vector", "settings_labs")
} }
/// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution.
public static var settingsLabsConfirmCryptoSdk: String {
return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk")
}
/// Create conference calls with jitsi /// Create conference calls with jitsi
public static var settingsLabsCreateConferenceWithJitsi: String { public static var settingsLabsCreateConferenceWithJitsi: String {
return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi")
} }
/// Rust end-to-end encryption (log out to disable)
public static var settingsLabsDisableCryptoSdk: String {
return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk")
}
/// End-to-End Encryption /// End-to-End Encryption
public static var settingsLabsE2eEncryption: String { public static var settingsLabsE2eEncryption: String {
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") return VectorL10n.tr("Vector", "settings_labs_e2e_encryption")
@ -7671,10 +7723,6 @@ public class VectorL10n: NSObject {
public static var settingsLabsEnableAutoReportDecryptionErrors: String { public static var settingsLabsEnableAutoReportDecryptionErrors: String {
return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors")
} }
/// Rust end-to-end encryption
public static var settingsLabsEnableCryptoSdk: String {
return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk")
}
/// Live location sharing - share current location (active development, and temporarily, locations persist in room history) /// Live location sharing - share current location (active development, and temporarily, locations persist in room history)
public static var settingsLabsEnableLiveLocationSharing: String { public static var settingsLabsEnableLiveLocationSharing: String {
return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing")

View file

@ -274,7 +274,7 @@ extension Analytics {
func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { func trackE2EEError(_ reason: DecryptionFailureReason, context: String) {
let event = AnalyticsEvent.Error( let event = AnalyticsEvent.Error(
context: context, context: context,
cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, cryptoModule: .Rust,
domain: .E2EE, domain: .E2EE,
name: reason.errorName name: reason.errorName
) )

View file

@ -46,9 +46,6 @@ struct SentryMonitoringClient {
if let message = event.message?.formatted { if let message = event.message?.formatted {
event.fingerprint = [message] event.fingerprint = [message]
} }
event.tags = [
"crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId
]
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
return event return event
} }

View file

@ -33,7 +33,6 @@
#import "ContactDetailsViewController.h" #import "ContactDetailsViewController.h"
#import "BugReportViewController.h" #import "BugReportViewController.h"
#import "RoomKeyRequestViewController.h"
#import "DecryptionFailureTracker.h" #import "DecryptionFailureTracker.h"
#import "Tools.h" #import "Tools.h"
@ -114,11 +113,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
id roomKeyRequestObserver; id roomKeyRequestObserver;
id roomKeyRequestCancellationObserver; id roomKeyRequestCancellationObserver;
/**
If any the currently displayed sharing key dialog
*/
RoomKeyRequestViewController *roomKeyRequestViewController;
/** /**
Incoming key verification requests observers Incoming key verification requests observers
*/ */
@ -396,6 +390,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
} }
[NSBundle mxk_setLanguage:language]; [NSBundle mxk_setLanguage:language];
[NSBundle mxk_setFallbackLanguage:@"en"]; [NSBundle mxk_setFallbackLanguage:@"en"];
UIApplication.sharedApplication.accessibilityLanguage = language;
if (BuildSettings.disableRightToLeftLayout) if (BuildSettings.disableRightToLeftLayout)
{ {
@ -1823,8 +1818,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// start the call service // start the call service
[self.callPresenter start]; [self.callPresenter start];
[self.configuration setupSettingsWhenLoadedFor:mxSession];
// Register to user new device sign in notification // Register to user new device sign in notification
[self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession]; [self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession];
@ -1833,8 +1826,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Register to new key verification request // Register to new key verification request
[self registerNewRequestNotificationForSession:mxSession]; [self registerNewRequestNotificationForSession:mxSession];
[self checkLocalPrivateKeysInSession:mxSession];
[self.pushNotificationService checkPushKitPushersInSession:mxSession]; [self.pushNotificationService checkPushKitPushersInSession:mxSession];
} }
else if (mxSession.state == MXSessionStateRunning) else if (mxSession.state == MXSessionStateRunning)
@ -2031,9 +2022,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// If any, disable the no VoIP support workaround // If any, disable the no VoIP support workaround
[self disableNoVoIPOnMatrixSession:mxSession]; [self disableNoVoIPOnMatrixSession:mxSession];
// Disable listening of incoming key share requests
[self disableRoomKeyRequestObserver:mxSession];
// Disable listening of incoming key verification requests // Disable listening of incoming key verification requests
[self disableIncomingKeyVerificationObserver:mxSession]; [self disableIncomingKeyVerificationObserver:mxSession];
@ -2183,9 +2171,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Clear cache // Clear cache
[self clearCache]; [self clearCache];
// Reset Crypto SDK configuration (labs flag for which crypto module to use)
[CryptoSDKFeature.shared reset];
// Reset key backup banner preferences // Reset key backup banner preferences
[SecureBackupBannerPreferences.shared reset]; [SecureBackupBannerPreferences.shared reset];
@ -2296,11 +2281,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
case MXSessionStateSyncInProgress: case MXSessionStateSyncInProgress:
// Stay in launching during the first server sync if the store is empty. // Stay in launching during the first server sync if the store is empty.
isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView);
if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists && [mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
[(MXLegacyCrypto *)mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil];
}
break; break;
case MXSessionStateRunning: case MXSessionStateRunning:
self.clearingCache = NO; self.clearingCache = NO;
@ -2360,7 +2340,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// This is the time to check existing requests // This is the time to check existing requests
MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests");
[self checkPendingRoomKeyRequests];
[self checkPendingIncomingKeyVerificationsInSession:mainSession]; [self checkPendingIncomingKeyVerificationsInSession:mainSession];
// TODO: When we will have an application state, we will do all of this in a dedicated initialisation state // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state
@ -2369,9 +2348,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{ {
MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module");
// Enable listening of incoming key share requests
[self enableRoomKeyRequestObserver:mainSession];
// Enable listening of incoming key verification requests // Enable listening of incoming key verification requests
[self enableIncomingKeyVerificationObserver:mainSession]; [self enableIncomingKeyVerificationObserver:mainSession];
} }
@ -2397,16 +2373,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{ {
MXLogDebug(@"[AppDelegate] showLaunchAnimation"); MXLogDebug(@"[AppDelegate] showLaunchAnimation");
LaunchLoadingView *launchLoadingView;
if (MXSDKOptions.sharedInstance.enableStartupProgress)
{
MXSession *mainSession = self.mxSessions.firstObject; MXSession *mainSession = self.mxSessions.firstObject;
launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress];
}
else
{
launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil];
}
launchLoadingView.frame = window.bounds; launchLoadingView.frame = window.bounds;
[launchLoadingView updateWithTheme:ThemeService.shared.theme]; [launchLoadingView updateWithTheme:ThemeService.shared.theme];
@ -2520,38 +2488,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
#endif #endif
} }
- (void)checkLocalPrivateKeysInSession:(MXSession*)mxSession
{
if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
return;
}
MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto;
MXRecoveryService *recoveryService = mxSession.crypto.recoveryService;
NSUInteger keysCount = 0;
if ([recoveryService hasSecretWithSecretId:MXSecretId.keyBackup])
{
keysCount++;
}
if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningUserSigning])
{
keysCount++;
}
if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningSelfSigning])
{
keysCount++;
}
if ((keysCount > 0 && keysCount < 3)
|| (mxSession.crypto.crossSigning.canTrustCrossSigning && !mxSession.crypto.crossSigning.canCrossSign))
{
// We should have 3 of them. If not, request them again as mitigation
MXLogDebug(@"[AppDelegate] checkLocalPrivateKeysInSession: request keys because keysCount = %@", @(keysCount));
[crypto requestAllPrivateKeys];
}
}
- (void)authenticationDidComplete - (void)authenticationDidComplete
{ {
[self handleAppState]; [self handleAppState];
@ -3461,173 +3397,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
} }
} }
#pragma mark - Incoming room key requests handling
- (void)enableRoomKeyRequestObserver:(MXSession*)mxSession
{
roomKeyRequestObserver =
[[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestNotification
object:mxSession.crypto
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *notif)
{
[self checkPendingRoomKeyRequestsInSession:mxSession];
}];
roomKeyRequestCancellationObserver =
[[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestCancellationNotification
object:mxSession.crypto
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *notif)
{
[self checkPendingRoomKeyRequestsInSession:mxSession];
}];
}
- (void)disableRoomKeyRequestObserver:(MXSession*)mxSession
{
if (roomKeyRequestObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestObserver];
roomKeyRequestObserver = nil;
}
if (roomKeyRequestCancellationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestCancellationObserver];
roomKeyRequestCancellationObserver = nil;
}
}
// Check if a key share dialog must be displayed for the given session
- (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
{
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive)
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession called while the app is not active. Ignore it.");
return;
}
if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Only legacy crypto allows manually accepting/rejecting key requests");
return;
}
MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto;
MXWeakify(self);
[crypto pendingKeyRequests:^(MXUsersDevicesMap<NSArray<MXIncomingRoomKeyRequest *> *> *pendingKeyRequests) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@",
crypto.crossSigning.state,
@(pendingKeyRequests.count),
self->roomKeyRequestViewController ? @"YES" : @"NO");
if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
{
if (self->roomKeyRequestViewController)
{
// Check if the current RoomKeyRequestViewController is still valid
MXSession *currentMXSession = self->roomKeyRequestViewController.mxSession;
NSString *currentUser = self->roomKeyRequestViewController.device.userId;
NSString *currentDevice = self->roomKeyRequestViewController.device.deviceId;
NSArray<MXIncomingRoomKeyRequest *> *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser];
if (currentMXSession == mxSession && currentPendingRequest.count == 0)
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog");
// The key request has been probably cancelled, remove the popup
[self->roomKeyRequestViewController hide];
self->roomKeyRequestViewController = nil;
}
}
}
if (!self->roomKeyRequestViewController && pendingKeyRequests.count)
{
// Pick the first coming user/device pair
NSString *userId = pendingKeyRequests.userIds.firstObject;
NSString *deviceId = [pendingKeyRequests deviceIdsForUser:userId].firstObject;
// Give the client a chance to refresh the device list
MXWeakify(self);
[crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap<MXDeviceInfo *> *usersDevicesInfoMap, NSDictionary<NSString *,MXCrossSigningInfo *> *crossSigningKeysMap) {
MXStrongifyAndReturnIfNil(self);
MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId];
if (deviceInfo)
{
if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
{
BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown);
void (^openDialog)(void) = ^void()
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo);
self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{
self->roomKeyRequestViewController = nil;
// Check next pending key request, if any
[self checkPendingRoomKeyRequests];
}];
[self->roomKeyRequestViewController show];
};
// If the device was new before, it's not any more.
if (wasNewDevice)
{
[crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil];
}
else
{
openDialog();
}
}
else if (deviceInfo.trustLevel.isVerified)
{
[crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
else
{
[crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
}
else
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId);
[crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
} failure:^(NSError *error) {
// Retry later
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Failed to download device keys. Retry");
[self checkPendingRoomKeyRequests];
}];
}
}];
}
// Check all opened MXSessions for key share dialog
- (void)checkPendingRoomKeyRequests
{
for (MXSession *mxSession in mxSessionArray)
{
[self checkPendingRoomKeyRequestsInSession:mxSession];
}
}
#pragma mark - Incoming key verification handling #pragma mark - Incoming key verification handling
- (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession - (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession
@ -3785,12 +3554,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId
{ {
id<MXCrypto> crypto = coordinatorBridgePresenter.session.crypto;
if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled))
{
MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys");
[(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil];
}
[self dismissKeyVerificationCoordinatorBridgePresenter]; [self dismissKeyVerificationCoordinatorBridgePresenter];
} }

View file

@ -130,8 +130,15 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
} }
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
// Check if the user must select a server
if BuildSettings.forceHomeserverSelection, authenticationService.provisioningLink?.homeserverUrl == nil {
showServerSelectionScreen(for: flow)
return
}
do { do {
// Start the flow using the default server (or a provisioning link if set). // Start the flow (if homeserverAddress is nil, the default server will be used).
try await authenticationService.startFlow(flow) try await authenticationService.startFlow(flow)
} catch { } catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.")
@ -613,8 +620,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
/// Replace the contents of the navigation router with a loading animation. /// Replace the contents of the navigation router with a loading animation.
private func showLoadingAnimation() { private func showLoadingAnimation() {
let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress)
let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
loadingViewController.modalPresentationStyle = .fullScreen loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation // Replace the navigation stack with the loading animation
@ -759,12 +765,6 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate {
// MARK: - KeyVerificationCoordinatorDelegate // MARK: - KeyVerificationCoordinatorDelegate
extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup,
!backup.hasPrivateKeyInCryptoStore || !backup.enabled {
MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys")
crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
}
navigationRouter.dismissModule(animated: true) { [weak self] in navigationRouter.dismissModule(animated: true) { [weak self] in
self?.authenticationDidComplete() self?.authenticationDidComplete()
} }

View file

@ -132,7 +132,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
target:self target:self
action:@selector(onButtonPressed:)]; action:@selector(onButtonPressed:)];
if (BuildSettings.forceHomeserverSelection)
{
self.defaultHomeServerUrl = nil;
}
else
{
self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString; self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString;
}
self.defaultIdentityServerUrl = RiotSettings.shared.identityServerUrlString; self.defaultIdentityServerUrl = RiotSettings.shared.identityServerUrlString;
@ -1207,7 +1214,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
[self saveCustomServerInputs]; [self saveCustomServerInputs];
// Restore default configuration // Restore default configuration
if (BuildSettings.forceHomeserverSelection)
{
[self setHomeServerTextFieldText:nil];
}
else
{
[self setHomeServerTextFieldText:self.defaultHomeServerUrl]; [self setHomeServerTextFieldText:self.defaultHomeServerUrl];
}
[self setIdentityServerTextFieldText:self.defaultIdentityServerUrl]; [self setIdentityServerTextFieldText:self.defaultIdentityServerUrl];
[self.customServersTickButton setImage:AssetImages.selectionUntick.image forState:UIControlStateNormal]; [self.customServersTickButton setImage:AssetImages.selectionUntick.image forState:UIControlStateNormal];

View file

@ -106,8 +106,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
// MARK: - Private // MARK: - Private
private func showLoadingAnimation() { private func showLoadingAnimation() {
let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress)
let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
loadingViewController.modalPresentationStyle = .fullScreen loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation // Replace the navigation stack with the loading animation
@ -220,12 +219,6 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate
// MARK: - KeyVerificationCoordinatorDelegate // MARK: - KeyVerificationCoordinatorDelegate
extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate { extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup,
!backup.hasPrivateKeyInCryptoStore || !backup.enabled {
MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys")
crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
}
navigationRouter.dismissModule(animated: true) { [weak self] in navigationRouter.dismissModule(animated: true) { [weak self] in
self?.authenticationDidComplete() self?.authenticationDidComplete()
} }

View file

@ -68,14 +68,7 @@ class SessionVerificationListener {
return return
} }
if session.state == .storeDataReady { if session.state == .running {
if let crypto = session.crypto as? MXLegacyCrypto {
// Do not make key share requests while the "Complete security" is not complete.
// If the device is self-verified, the SDK will restore the existing key backup.
// Then, it will re-enable outgoing key share requests
crypto.setOutgoingKeyRequestsEnabled(false, onComplete: nil)
}
} else if session.state == .running {
unregisterSessionStateChangeNotification() unregisterSessionStateChangeNotification()
if let crypto = session.crypto { if let crypto = session.crypto {
@ -101,7 +94,6 @@ class SessionVerificationListener {
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} failure: { error in } failure: { error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error)
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} }
} else { } else {
@ -111,12 +103,10 @@ class SessionVerificationListener {
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} failure: { error in } failure: { error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.")
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} }
} }
} else { } else {
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} }
case .crossSigningExists: case .crossSigningExists:
@ -124,13 +114,10 @@ class SessionVerificationListener {
self.completion?(.needsVerification) self.completion?(.needsVerification)
default: default:
MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do")
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete) self.completion?(.authenticationIsComplete)
} }
} failure: { [weak self] error in } failure: { [weak self] error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error)
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self?.completion?(.authenticationIsComplete) self?.completion?(.authenticationIsComplete)
} }
} else { } else {

View file

@ -371,17 +371,6 @@ CallAudioRouteMenuViewDelegate>
typeof(self) self = weakSelf; typeof(self) self = weakSelf;
self->currentAlert = nil; self->currentAlert = nil;
// Acknowledge the existence of all devices
[self startActivityIndicator];
if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices");
return;
}
[(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{
[self stopActivityIndicator];
// Retry the call // Retry the call
if (call.isIncoming) if (call.isIncoming)
{ {
@ -391,7 +380,6 @@ CallAudioRouteMenuViewDelegate>
{ {
[call callWithVideo:call.isVideoCall]; [call callWithVideo:call.isVideoCall];
} }
}];
} }
}]]; }]];

View file

@ -103,12 +103,17 @@ class AvatarView: UIView, Themable {
func updateAvatarImageView(with viewData: AvatarViewDataProtocol) { func updateAvatarImageView(with viewData: AvatarViewDataProtocol) {
guard let avatarImageView = self.avatarImageView else { guard let avatarImageView = self.avatarImageView else {
MXLog.warning("[AvatarView] avatar not updated because avatarImageView is nil.")
return return
} }
let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill) let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill)
updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
if defaultAvatarImage == nil {
MXLog.warning("[AvatarView] defaultAvatarImage is nil")
}
if let avatarUrl = viewData.avatarUrl { if let avatarUrl = viewData.avatarUrl {
avatarImageView.setImageURI(avatarUrl, avatarImageView.setImageURI(avatarUrl,
withType: nil, withType: nil,
@ -118,6 +123,10 @@ class AvatarView: UIView, Themable {
previewImage: defaultAvatarImage, previewImage: defaultAvatarImage,
mediaManager: viewData.mediaManager) mediaManager: viewData.mediaManager)
updateAvatarContentMode(contentMode: .scaleAspectFill) updateAvatarContentMode(contentMode: .scaleAspectFill)
if avatarImageView.frame.size.width < 8 || avatarImageView.frame.size.height < 8 {
MXLog.warning("[AvatarView] small avatarImageView frame: \(avatarImageView.frame)")
}
} else { } else {
updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
} }

View file

@ -81,7 +81,8 @@
// Manage lastEventAttributedTextMessage optional property // Manage lastEventAttributedTextMessage optional property
if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)]) if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)])
{ {
self.lastEventDescription.attributedText = roomCellData.lastEventAttributedTextMessage; // Attempt to correct the attributed string colors to match the current theme
self.lastEventDescription.attributedText = [roomCellData.lastEventAttributedTextMessage fixForegroundColor];
} }
else else
{ {

View file

@ -0,0 +1,68 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Object responsible for calculating user and room trust level
///
/// For legacy reasons, the trust of multiple items is represented as `Progress` object,
/// where `completedUnitCount` represents the number of trusted users / devices.
@objc class EncryptionTrustLevel: NSObject {
struct TrustSummary {
let totalCount: Int64
let trustedCount: Int64
let areAllTrusted: Bool
init(progress: Progress) {
totalCount = max(progress.totalUnitCount, progress.completedUnitCount)
trustedCount = progress.completedUnitCount
areAllTrusted = trustedCount == totalCount
}
}
/// Calculate trust level for a single user given their cross-signing info
@objc func userTrustLevel(
crossSigning: MXCrossSigningInfo?,
trustedDevicesProgress: Progress
) -> UserEncryptionTrustLevel {
let devices = TrustSummary(progress: trustedDevicesProgress)
// If we could cross-sign but we haven't, the user is simply not verified
if let crossSigning, !crossSigning.trustLevel.isVerified {
return .notVerified
// If we cannot cross-sign the user (legacy behaviour) and have not signed
// any devices manually, the user is not verified
} else if crossSigning == nil && devices.trustedCount == 0 {
return .notVerified
}
// In all other cases we check devices for trust level
return devices.areAllTrusted ? .trusted : .warning
}
/// Calculate trust level for a room given trust level of users and their devices
@objc func roomTrustLevel(summary: MXUsersTrustLevelSummary) -> RoomEncryptionTrustLevel {
let users = TrustSummary(progress: summary.trustedUsersProgress)
let devices = TrustSummary(progress: summary.trustedDevicesProgress)
guard users.totalCount > 0 && users.areAllTrusted else {
return .normal
}
return devices.areAllTrusted ? .trusted : .warning
}
}

View file

@ -1,5 +1,5 @@
// //
// Copyright 2021 New Vector Ltd // Copyright 2023 New Vector Ltd
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -14,22 +14,12 @@
// limitations under the License. // limitations under the License.
// //
import Foundation /**
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
enum UserSuggestionViewAction { */
case selectedItem(UserSuggestionViewStateItem) typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
} RoomEncryptionTrustLevelTrusted,
RoomEncryptionTrustLevelWarning,
enum UserSuggestionViewModelResult { RoomEncryptionTrustLevelNormal,
case selectedItemWithIdentifier(String) RoomEncryptionTrustLevelUnknown
} };
struct UserSuggestionViewStateItem: Identifiable {
let id: String
let avatar: AvatarInputProtocol?
let displayName: String?
}
struct UserSuggestionViewState: BindableState {
var items: [UserSuggestionViewStateItem]
}

View file

@ -387,7 +387,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
} }
private func updateAvatarButtonItem() { private func updateAvatarButtonItem() {
MXLog.info("[AllChatsCoordinator] updating avatar button item.")
if let avatar = userAvatarViewData(from: currentMatrixSession) { if let avatar = userAvatarViewData(from: currentMatrixSession) {
if avatarMenuView == nil {
MXLog.warning("[AllChatsCoordinator] updateAvatarButtonItem: avatarMenuView is nil.")
}
avatarMenuView?.fill(with: avatar) avatarMenuView?.fill(with: avatar)
avatarMenuButton?.setImage(nil, for: .normal) avatarMenuButton?.setImage(nil, for: .normal)
} else { } else {

View file

@ -988,8 +988,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol {
let title: String let title: String
let message: String let message: String
if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature, if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true {
feature.isEnabled && feature.needsVerificationUpgrade {
title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle
message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage
} else { } else {

View file

@ -24,4 +24,9 @@ class VerifyEmojiCollectionViewCell: UICollectionViewCell, Reusable, Themable {
func update(theme: Theme) { func update(theme: Theme) {
name.textColor = theme.textPrimaryColor name.textColor = theme.textPrimaryColor
} }
override func awakeFromNib() {
super.awakeFromNib()
emoji.isAccessibilityElement = false
}
} }

View file

@ -69,9 +69,6 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
extension LaunchLoadingView: MXSessionStartupProgressDelegate { extension LaunchLoadingView: MXSessionStartupProgressDelegate {
func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) { func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) {
guard MXSDKOptions.sharedInstance().enableStartupProgress else {
return
}
update(with: state) update(with: state)
} }

View file

@ -17,3 +17,4 @@
#import "MXKRoomBubbleCellData.h" #import "MXKRoomBubbleCellData.h"
#import "UserIndicatorCancel.h" #import "UserIndicatorCancel.h"
#import "VoiceBroadcastInfo.h" #import "VoiceBroadcastInfo.h"
#import "MXKSoundPlayer.h"

View file

@ -145,5 +145,3 @@
#import "MXKCountryPickerViewController.h" #import "MXKCountryPickerViewController.h"
#import "MXKLanguagePickerViewController.h" #import "MXKLanguagePickerViewController.h"
#import "MXKSlashCommands.h"

View file

@ -947,14 +947,6 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
if (clearStore) if (clearStore)
{ {
// Force a reload of device keys at the next session start, unless we are just about to migrate
// all data and device keys into CryptoSDK.
// This will fix potential UISIs other peoples receive for our messages.
if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK)
{
[(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys];
}
// Clean other stores // Clean other stores
[mxSession.scanManager deleteAllAntivirusScans]; [mxSession.scanManager deleteAllAntivirusScans];
[mxSession.aggregations resetData]; [mxSession.aggregations resetData];

View file

@ -144,6 +144,15 @@
*/ */
- (CGFloat)rawTextHeight:(NSAttributedString*)attributedText; - (CGFloat)rawTextHeight:(NSAttributedString*)attributedText;
/**
Return the raw height of the provided text by removing any vertical margin/inset and constraining the width.
@param attributedText the attributed text to measure
@param maxTextViewWidth the maximum text width
@return the computed height
*/
- (CGFloat)rawTextHeight:(NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth;
/** /**
Return the content size of a text view initialized with the provided attributed text. Return the content size of a text view initialized with the provided attributed text.
CAUTION: This method runs only on main thread. CAUTION: This method runs only on main thread.

View file

@ -500,23 +500,34 @@
// Return the raw height of the provided text by removing any margin // Return the raw height of the provided text by removing any margin
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText - (CGFloat)rawTextHeight: (NSAttributedString*)attributedText
{
return [self rawTextHeight:attributedText withMaxWidth:_maxTextViewWidth];
}
// Return the raw height of the provided text by removing any vertical margin/inset and constraining the width.
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth
{ {
__block CGSize textSize; __block CGSize textSize;
if ([NSThread currentThread] != [NSThread mainThread]) if ([NSThread currentThread] != [NSThread mainThread])
{ {
dispatch_sync(dispatch_get_main_queue(), ^{ dispatch_sync(dispatch_get_main_queue(), ^{
textSize = [self textContentSize:attributedText removeVerticalInset:YES]; textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
}); });
} }
else else
{ {
textSize = [self textContentSize:attributedText removeVerticalInset:YES]; textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
} }
return textSize.height; return textSize.height;
} }
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset - (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset
{
return [self textContentSize:attributedText removeVerticalInset:removeVerticalInset maxTextViewWidth:_maxTextViewWidth];
}
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset maxTextViewWidth:(CGFloat)maxTextViewWidth
{ {
static UITextView* measurementTextView = nil; static UITextView* measurementTextView = nil;
static UITextView* measurementTextViewWithoutInset = nil; static UITextView* measurementTextViewWithoutInset = nil;
@ -536,7 +547,7 @@
// Select the right text view for measurement // Select the right text view for measurement
UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView); UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView);
selectedTextView.frame = CGRectMake(0, 0, _maxTextViewWidth, 0); selectedTextView.frame = CGRectMake(0, 0, maxTextViewWidth, 0);
selectedTextView.attributedText = attributedText; selectedTextView.attributedText = attributedText;
// Force the layout manager to layout the text, fixes problems starting iOS 16 // Force the layout manager to layout the text, fixes problems starting iOS 16

View file

@ -31,8 +31,6 @@
#import "MXKAppSettings.h" #import "MXKAppSettings.h"
#import "MXKSlashCommands.h"
#import "GeneratedInterface-Swift.h" #import "GeneratedInterface-Swift.h"
const BOOL USE_THREAD_TIMELINE = YES; const BOOL USE_THREAD_TIMELINE = YES;
@ -316,7 +314,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
_filterMessagesWithURL = NO; _filterMessagesWithURL = NO;
emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", kMXKSlashCmdEmote]; emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]];
// Set default data and view classes // Set default data and view classes
// Cell data // Cell data
@ -458,11 +456,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
} }
- (void)reset - (void)reset
{
[self resetNotifying:YES];
}
- (void)resetNotifying:(BOOL)notify
{ {
if (roomDidFlushDataNotificationObserver) if (roomDidFlushDataNotificationObserver)
{ {
@ -558,12 +551,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
} }
_serverSyncEventCount = 0; _serverSyncEventCount = 0;
// Notify the delegate to reload its tableview
if (notify && self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
} }
- (void)reload - (void)reload
@ -577,10 +564,16 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
[self setState:MXKDataSourceStatePreparing]; [self setState:MXKDataSourceStatePreparing];
[self resetNotifying:notify]; [self reset];
// Reload // Reload
[self didMXSessionStateChange]; [self didMXSessionStateChange];
// Notify the delegate to refresh the tableview
if (notify && self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
} }
- (void)destroy - (void)destroy

View file

@ -1,34 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
@import Foundation;
/**
Slash commands used to perform actions from a room.
*/
FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeDisplayName;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdEmote;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdJoinRoom;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdPartRoom;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdInviteUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdKickUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdBanUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdUnbanUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdSetUserPowerLevel;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdResetUserPowerLevel;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeRoomTopic;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdDiscardSession;

View file

@ -1,30 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSlashCommands.h"
NSString *const kMXKSlashCmdChangeDisplayName = @"/nick";
NSString *const kMXKSlashCmdEmote = @"/me";
NSString *const kMXKSlashCmdJoinRoom = @"/join";
NSString *const kMXKSlashCmdPartRoom = @"/part";
NSString *const kMXKSlashCmdInviteUser = @"/invite";
NSString *const kMXKSlashCmdKickUser = @"/kick";
NSString *const kMXKSlashCmdBanUser = @"/ban";
NSString *const kMXKSlashCmdUnbanUser = @"/unban";
NSString *const kMXKSlashCmdSetUserPowerLevel = @"/op";
NSString *const kMXKSlashCmdResetUserPowerLevel = @"/deop";
NSString *const kMXKSlashCmdChangeRoomTopic = @"/topic";
NSString *const kMXKSlashCmdDiscardSession = @"/discardsession";

View file

@ -0,0 +1,101 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
@objc final class MXKSlashCommandsHelper: NSObject {
@objc static func commandNameFor(_ slashCommand: MXKSlashCommand) -> String {
slashCommand.cmd
}
@objc static func commandUsageFor(_ slashCommand: MXKSlashCommand) -> String {
"Usage: \(slashCommand.cmd) \(slashCommand.parametersFormat)"
}
}
@objc enum MXKSlashCommand: Int, CaseIterable {
case changeDisplayName
case emote
case joinRoom
case partRoom
case inviteUser
case kickUser
case banUser
case unbanUser
case setUserPowerLevel
case resetUserPowerLevel
case changeRoomTopic
case discardSession
var cmd: String {
switch self {
case .changeDisplayName:
return "/nick"
case .emote:
return "/me"
case .joinRoom:
return "/join"
case .partRoom:
return "/part"
case .inviteUser:
return "/invite"
case .kickUser:
return "/kick"
case .banUser:
return "/ban"
case .unbanUser:
return "/unban"
case .setUserPowerLevel:
return "/op"
case .resetUserPowerLevel:
return "/deop"
case .changeRoomTopic:
return "/topic"
case .discardSession:
return "/discardsession"
}
}
// Note: not localized for consistency, as commands are in english
// also translating these parameters could lead to inconsistency in
// the UI in case of languages with overlength translation.
var parametersFormat: String {
switch self {
case .changeDisplayName:
return "<display_name>"
case .emote:
return "<message>"
case .joinRoom:
return "<room-address>"
case .partRoom:
return "[<room-address>]"
case .inviteUser:
return "<user-id>"
case .kickUser:
return "<user-id> [<reason>]"
case .banUser:
return "<user-id> [<reason>]"
case .unbanUser:
return "<user-id>"
case .setUserPowerLevel:
return "<user-id> <power-level>"
case .resetUserPowerLevel:
return "<user-id>"
case .changeRoomTopic:
return "<topic>"
case .discardSession:
return ""
}
}
}

View file

@ -571,7 +571,7 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
} }
else else
{ {
displayText = [VectorL10n noticeDisplayNameChangedFrom:event.sender :prevDisplayname :displayname]; displayText = [VectorL10n noticeDisplayNameChangedTo:prevDisplayname :displayname];
} }
} }
} }

View file

@ -102,6 +102,14 @@ typedef enum : NSUInteger
*/ */
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText; - (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText;
/**
Tells the delegate that the user wants to send a command.
@param toolbarView the room input toolbar view.
@param commandText the command to send.
*/
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText;
/** /**
Tells the delegate that the user wants to display the send media actions. Tells the delegate that the user wants to display the send media actions.
@ -205,6 +213,15 @@ typedef enum : NSUInteger
*/ */
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView updateActivityIndicator:(BOOL)isAnimating; - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView updateActivityIndicator:(BOOL)isAnimating;
/**
Tells the delegate that the partial content of the composer has changed
and should be stored to allow restoring it later if needed.
@param toolbarView the room input toolbar view
@param partialAttributedTextMessage the partial content to store
*/
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView shouldStorePartialContent:(NSAttributedString*)partialAttributedTextMessage;
@end @end
/** /**
@ -382,6 +399,11 @@ typedef enum : NSUInteger
*/ */
@property (nonatomic) NSAttributedString *attributedTextMessage; @property (nonatomic) NSAttributedString *attributedTextMessage;
/**
Sets the partial text message to apply to the current message composer.
*/
- (void)setPartialContent:(NSAttributedString *)attributedTextMessage;
/** /**
Default font for the message composer. Default font for the message composer.
*/ */

View file

@ -1405,4 +1405,9 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes)
return NO; return NO;
} }
- (void)setPartialContent:(NSAttributedString *)attributedTextMessage
{
self.attributedTextMessage = attributedTextMessage;
}
@end @end

View file

@ -19,7 +19,7 @@ import Foundation
extension RoomDataSource { extension RoomDataSource {
// MARK: - Private Constants // MARK: - Private Constants
private enum Constants { private enum Constants {
static let emoteMessageSlashCommandPrefix = String(format: "%@ ", kMXKSlashCmdEmote) static let emoteMessageSlashCommandPrefix = String(format: "%@ ", MXKSlashCommand.emote.cmd)
} }
// MARK: - NSAttributedString Sending // MARK: - NSAttributedString Sending

View file

@ -39,7 +39,6 @@
#import "MXKEncryptionKeysImportView.h" #import "MXKEncryptionKeysImportView.h"
#import "NSBundle+MatrixKit.h" #import "NSBundle+MatrixKit.h"
#import "MXKSlashCommands.h"
#import "MXKSwiftHeader.h" #import "MXKSwiftHeader.h"
#import "MXKPreviewViewController.h" #import "MXKPreviewViewController.h"
@ -361,7 +360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
{ {
// Retrieve the potential message partially typed during last room display. // Retrieve the potential message partially typed during last room display.
// Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; [inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
} }
if (!hasAppearedOnce) if (!hasAppearedOnce)
@ -1285,7 +1284,13 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
// TODO: display an alert with the cmd usage in case of error or unrecognized cmd. // TODO: display an alert with the cmd usage in case of error or unrecognized cmd.
NSString *cmdUsage; NSString *cmdUsage;
if ([cmd isEqualToString:kMXKSlashCmdEmote]) NSString* kMXKSlashCmdChangeDisplayName = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeDisplayName];
NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
NSString* kMXKSlashCmdPartRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandPartRoom];
NSString* kMXKSlashCmdChangeRoomTopic = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeRoomTopic];
if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
{ {
// send message as an emote // send message as an emote
[self sendTextMessage:string]; [self sendTextMessage:string];
@ -1320,7 +1325,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /nick <display_name>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName];
} }
} }
else if ([string hasPrefix:kMXKSlashCmdJoinRoom]) else if ([string hasPrefix:kMXKSlashCmdJoinRoom])
@ -1355,7 +1360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /join <room_alias>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
} }
} }
else if ([string hasPrefix:kMXKSlashCmdPartRoom]) else if ([string hasPrefix:kMXKSlashCmdPartRoom])
@ -1413,7 +1418,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /part [<room_alias>]"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom];
} }
} }
else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic]) else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic])
@ -1445,10 +1450,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /topic <topic>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeRoomTopic];
} }
} }
else if ([string hasPrefix:kMXKSlashCmdDiscardSession]) else if ([string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandDiscardSession]])
{ {
[roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{ [roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{
MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session"); MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session");
@ -1470,7 +1475,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
userId = nil; userId = nil;
} }
if ([cmd isEqualToString:kMXKSlashCmdInviteUser]) if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]])
{ {
if (userId) if (userId)
{ {
@ -1489,10 +1494,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /invite <userId>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandInviteUser];
} }
} }
else if ([cmd isEqualToString:kMXKSlashCmdKickUser]) else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandKickUser]])
{ {
if (userId) if (userId)
{ {
@ -1524,10 +1529,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /kick <userId> [<reason>]"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandKickUser];
} }
} }
else if ([cmd isEqualToString:kMXKSlashCmdBanUser]) else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandBanUser]])
{ {
if (userId) if (userId)
{ {
@ -1559,10 +1564,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /ban <userId> [<reason>]"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandBanUser];
} }
} }
else if ([cmd isEqualToString:kMXKSlashCmdUnbanUser]) else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandUnbanUser]])
{ {
if (userId) if (userId)
{ {
@ -1581,10 +1586,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /unban <userId>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandUnbanUser];
} }
} }
else if ([cmd isEqualToString:kMXKSlashCmdSetUserPowerLevel]) else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandSetUserPowerLevel]])
{ {
// Retrieve power level // Retrieve power level
NSString *powerLevel = nil; NSString *powerLevel = nil;
@ -1617,10 +1622,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /op <userId> <power level>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandSetUserPowerLevel];
} }
} }
else if ([cmd isEqualToString:kMXKSlashCmdResetUserPowerLevel]) else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandResetUserPowerLevel]])
{ {
if (userId) if (userId)
{ {
@ -1639,7 +1644,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
cmdUsage = @"Usage: /deop <userId>"; cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel];
} }
} }
else else

View file

@ -61,7 +61,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
// The preview header // The preview header
@property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; @property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *userSuggestionContainerHeightConstraint; @property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *completionSuggestionContainerHeightConstraint;
// The jump to last unread banner // The jump to last unread banner
@property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer; @property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer;

View file

@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate, @interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate, ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate, ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate> RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
{ {
// The preview header // The preview header
@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize;
@property (nonatomic, strong) ShareManager *shareManager; @property (nonatomic, strong) ShareManager *shareManager;
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder; @property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize;
[self setupActions]; [self setupActions];
[self setupUserSuggestionViewIfNeeded]; [self setupCompletionSuggestionViewIfNeeded];
[self.topBannersStackView vc_removeAllSubviews]; [self.topBannersStackView vc_removeAllSubviews];
} }
@ -693,7 +693,7 @@ static CGSize kThreadListBarButtonItemImageSize;
{ {
// Retrieve the potential message partially typed during last room display. // Retrieve the potential message partially typed during last room display.
// Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage; [self.inputToolbarView setPartialContent:self.roomDataSource.partialAttributedTextMessage];
} }
[self setMaximisedToolbarIsHiddenIfNeeded: NO]; [self setMaximisedToolbarIsHiddenIfNeeded: NO];
@ -1088,12 +1088,14 @@ static CGSize kThreadListBarButtonItemImageSize;
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
_voiceMessageController.roomId = dataSource.roomId; _voiceMessageController.roomId = dataSource.roomId;
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager _completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
room:dataSource.room room:dataSource.room
userID:self.roomDataSource.mxSession.myUserId]; userID:self.roomDataSource.mxSession.myUserId];
_userSuggestionCoordinator.delegate = self; _completionSuggestionCoordinator.delegate = self;
[self setupUserSuggestionViewIfNeeded]; [self setupCompletionSuggestionViewIfNeeded];
[self updateRoomInputToolbarViewClassIfNeeded];
[self updateTopBanners]; [self updateTopBanners];
} }
@ -1195,6 +1197,12 @@ static CGSize kThreadListBarButtonItemImageSize;
{ {
Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass]; Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];
// If RTE is enabled, delay the toolbar setup until `completionSuggestionCoordinator` is ready.
if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && _completionSuggestionCoordinator == nil)
{
return;
}
BOOL shouldDismissContextualMenu = NO; BOOL shouldDismissContextualMenu = NO;
// Check the user has enough power to post message // Check the user has enough power to post message
@ -1282,6 +1290,8 @@ static CGSize kThreadListBarButtonItemImageSize;
{ {
// Override the default behavior for `/join` command in order to open automatically the joined room // Override the default behavior for `/join` command in order to open automatically the joined room
NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
if ([string hasPrefix:kMXKSlashCmdJoinRoom]) if ([string hasPrefix:kMXKSlashCmdJoinRoom])
{ {
// Join a room // Join a room
@ -1317,7 +1327,7 @@ static CGSize kThreadListBarButtonItemImageSize;
else else
{ {
// Display cmd usage in text input as placeholder // Display cmd usage in text input as placeholder
self.inputToolbarView.placeholder = @"Usage: /join <room_alias>"; self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
} }
return YES; return YES;
} }
@ -2726,13 +2736,13 @@ static CGSize kThreadListBarButtonItemImageSize;
} }
} }
- (void)setupUserSuggestionViewIfNeeded - (void)setupCompletionSuggestionViewIfNeeded
{ {
if(!self.isViewLoaded) { if(!self.isViewLoaded) {
return; return;
} }
UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
if (!suggestionsViewController) if (!suggestionsViewController)
{ {
@ -2742,12 +2752,12 @@ static CGSize kThreadListBarButtonItemImageSize;
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addChildViewController:suggestionsViewController]; [self addChildViewController:suggestionsViewController];
[self.userSuggestionContainerView addSubview:suggestionsViewController.view]; [self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
[suggestionsViewController didMoveToParentViewController:self]; [suggestionsViewController didMoveToParentViewController:self];
} }
@ -5147,17 +5157,17 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
{ {
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; [self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
} }
- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern - (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern
{ {
[self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; [self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern];
} }
- (UserSuggestionViewModelContextWrapper *)userSuggestionContext - (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext
{ {
return [self.userSuggestionCoordinator sharedContext]; return [self.completionSuggestionCoordinator sharedContext];
} }
- (MXMediaManager *)mediaManager - (MXMediaManager *)mediaManager
@ -5188,6 +5198,27 @@ static CGSize kThreadListBarButtonItemImageSize;
}]; }];
} }
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText
{
// Create before sending the message in case of a discussion (direct chat)
MXWeakify(self);
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
MXStrongifyAndReturnIfNil(self);
if (readyToSend) {
if (![self sendAsIRCStyleCommandIfPossible:commandText])
{
// Display an error for unknown command
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:[VectorL10n roomCommandErrorUnknownCommand]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
}
}];
}
- (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView - (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView
{ {
NSMutableArray *actionItems = [NSMutableArray new]; NSMutableArray *actionItems = [NSMutableArray new];
@ -5237,7 +5268,7 @@ static CGSize kThreadListBarButtonItemImageSize;
if (readyToSend) { if (readyToSend) {
BOOL isMessageAHandledCommand = NO; BOOL isMessageAHandledCommand = NO;
// "/me" command is supported with Pills in RoomDataSource. // "/me" command is supported with Pills in RoomDataSource.
if (![attributedTextMessage.string hasPrefix:kMXKSlashCmdEmote]) if (![attributedTextMessage.string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
{ {
// Other commands currently work with identifiers (e.g. ban, invite, op, etc). // Other commands currently work with identifiers (e.g. ban, invite, op, etc).
NSString *message; NSString *message;
@ -5262,6 +5293,11 @@ static CGSize kThreadListBarButtonItemImageSize;
}]; }];
} }
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage
{
self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage;
}
#pragma mark - MXKRoomMemberDetailsViewControllerDelegate #pragma mark - MXKRoomMemberDetailsViewControllerDelegate
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion - (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion
@ -6104,7 +6140,7 @@ static CGSize kThreadListBarButtonItemImageSize;
if (self.saveProgressTextInput) if (self.saveProgressTextInput)
{ {
// Restore the potential message partially typed before jump to last unread messages. // Restore the potential message partially typed before jump to last unread messages.
self.inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; [self.inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
} }
}; };
@ -6356,21 +6392,10 @@ static CGSize kThreadListBarButtonItemImageSize;
self->currentAlert = nil; self->currentAlert = nil;
// Acknowledge the existence of all devices // Acknowledge the existence of all devices
[self startActivityIndicator];
if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices");
return;
}
[(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{
self->unknownDevices = nil; self->unknownDevices = nil;
[self stopActivityIndicator];
// And resend pending messages // And resend pending messages
[self resendAllUnsentMessages]; [self resendAllUnsentMessages];
}];
} }
}]]; }]];
@ -7484,23 +7509,47 @@ static CGSize kThreadListBarButtonItemImageSize;
return; return;
} }
self.customizedRoomDataSource.highlightedEventId = eventId; NSMutableArray<NSIndexPath *> *rowsToReload = [[NSMutableArray alloc] init];
// Get the current hightlighted event because we will need to reload it
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; NSString *currentHiglightedEventId = self.customizedRoomDataSource.highlightedEventId;
if (currentHiglightedEventId)
{
NSInteger currentHiglightedRow = [self.roomDataSource indexOfCellDataWithEventId:currentHiglightedEventId];
if (currentHiglightedRow != NSNotFound)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentHiglightedRow inSection:0];
if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
{ {
[self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] [rowsToReload addObject:indexPath];
withRowAnimation:UITableViewRowAnimationNone];
[self.bubblesTableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:YES];
} }
else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) }
}
self.customizedRoomDataSource.highlightedEventId = eventId;
// Add the new highligted event to the list of rows to reload
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath];
if (indexPathIsVisible)
{
[rowsToReload addObject:indexPath];
}
// Reload rows
if (rowsToReload.count > 0)
{
[self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload
withRowAnimation:UITableViewRowAnimationNone];
}
// Scroll to the newly highlighted row
if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath])
{ {
[self.bubblesTableView scrollToRowAtIndexPath:indexPath [self.bubblesTableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle atScrollPosition:UITableViewScrollPositionMiddle
animated:YES]; animated:YES];
} }
if (completion) if (completion)
{ {
completion(); completion();
@ -8070,9 +8119,9 @@ static CGSize kThreadListBarButtonItemImageSize;
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
} }
#pragma mark - UserSuggestionCoordinatorBridgeDelegate #pragma mark - CompletionSuggestionCoordinatorBridgeDelegate
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator - (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestMentionForMember:(MXRoomMember *)member didRequestMentionForMember:(MXRoomMember *)member
textTrigger:(NSString *)textTrigger textTrigger:(NSString *)textTrigger
{ {
@ -8080,16 +8129,32 @@ static CGSize kThreadListBarButtonItemImageSize;
[self mention:member]; [self mention:member];
} }
- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator - (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
textTrigger:(NSString *)textTrigger textTrigger:(NSString *)textTrigger
{ {
[self removeTriggerTextFromComposer:textTrigger]; [self removeTriggerTextFromComposer:textTrigger];
[self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; [self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]];
}
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestCommand:(NSString *)command
textTrigger:(NSString *)textTrigger
{
[self removeTriggerTextFromComposer:textTrigger];
[self setCommand:command];
} }
- (void)removeTriggerTextFromComposer:(NSString *)textTrigger - (void)removeTriggerTextFromComposer:(NSString *)textTrigger
{ {
RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView;
Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];
// RTE handles removing the text trigger by itself.
if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && RiotSettings.shared.enableWysiwygTextFormatting)
{
return;
}
if (toolbar && textTrigger.length) { if (toolbar && textTrigger.length) {
NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage]; NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage];
[[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger [[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger
@ -8100,11 +8165,11 @@ static CGSize kThreadListBarButtonItemImageSize;
} }
} }
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height - (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
{ {
if (self.userSuggestionContainerHeightConstraint.constant != height) if (self.completionSuggestionContainerHeightConstraint.constant != height)
{ {
self.userSuggestionContainerHeightConstraint.constant = height; self.completionSuggestionContainerHeightConstraint.constant = height;
[self.view layoutIfNeeded]; [self.view layoutIfNeeded];
} }

View file

@ -58,6 +58,22 @@ extension RoomViewController {
} }
} }
@objc func setCommand(_ command: String) {
if let wysiwygInputToolbar, wysiwygInputToolbar.textFormattingEnabled {
wysiwygInputToolbar.command(command)
wysiwygInputToolbar.becomeFirstResponder()
} else {
guard let attributedText = inputToolbarView.attributedTextMessage else { return }
let newAttributedString = NSMutableAttributedString(attributedString: attributedText)
newAttributedString.append(NSAttributedString(string: "\(command) ",
attributes: [.font: inputToolbarView.defaultFont]))
inputToolbarView.attributedTextMessage = newAttributedString
inputToolbarView.becomeFirstResponder()
}
}
/// Send the formatted text message and its raw counterpart to the room /// Send the formatted text message and its raw counterpart to the room
/// ///
@ -91,7 +107,7 @@ extension RoomViewController {
"event_id": eventModified.eventId "event_id": eventModified.eventId
]) ])
}) })
} else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) { } else {
roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in
switch response { switch response {
case .success: case .success:

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -13,6 +12,8 @@
<connections> <connections>
<outlet property="bubblesTableView" destination="BGD-sd-SQR" id="OG4-Tw-Ovt"/> <outlet property="bubblesTableView" destination="BGD-sd-SQR" id="OG4-Tw-Ovt"/>
<outlet property="bubblesTableViewBottomConstraint" destination="1SD-y2-oTg" id="n8D-hT-eqt"/> <outlet property="bubblesTableViewBottomConstraint" destination="1SD-y2-oTg" id="n8D-hT-eqt"/>
<outlet property="completionSuggestionContainerHeightConstraint" destination="1Cd-cT-gOr" id="au5-3q-r54"/>
<outlet property="completionSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
<outlet property="inputBackgroundView" destination="Xt7-83-dQh" id="xoG-eb-zFB"/> <outlet property="inputBackgroundView" destination="Xt7-83-dQh" id="xoG-eb-zFB"/>
<outlet property="jumpToLastUnreadBanner" destination="S6r-bo-jxw" id="FSS-Be-E15"/> <outlet property="jumpToLastUnreadBanner" destination="S6r-bo-jxw" id="FSS-Be-E15"/>
<outlet property="jumpToLastUnreadBannerContainer" destination="S6H-Az-RCM" id="YlI-fu-OpT"/> <outlet property="jumpToLastUnreadBannerContainer" destination="S6H-Az-RCM" id="YlI-fu-OpT"/>
@ -32,8 +33,6 @@
<outlet property="scrollToBottomBadgeLabel" destination="QHs-rM-UU8" id="wk7-PQ-9Jm"/> <outlet property="scrollToBottomBadgeLabel" destination="QHs-rM-UU8" id="wk7-PQ-9Jm"/>
<outlet property="scrollToBottomButton" destination="Ih9-EU-BOU" id="Wwg-gS-Sfp"/> <outlet property="scrollToBottomButton" destination="Ih9-EU-BOU" id="Wwg-gS-Sfp"/>
<outlet property="topBannersStackView" destination="3z2-8P-wlg" id="uf5-gw-zWi"/> <outlet property="topBannersStackView" destination="3z2-8P-wlg" id="uf5-gw-zWi"/>
<outlet property="userSuggestionContainerHeightConstraint" destination="1Cd-cT-gOr" id="au5-3q-r54"/>
<outlet property="userSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/> <outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
<outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/> <outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/>
<outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/> <outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/>
@ -48,20 +47,20 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="3z2-8P-wlg" userLabel="Top Banners Stack View"> <stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="3z2-8P-wlg" userLabel="Top Banners Stack View">
<rect key="frame" x="0.0" y="0.0" width="375" height="0.0"/> <rect key="frame" x="0.0" y="20" width="375" height="0.0"/>
<constraints> <constraints>
<constraint firstAttribute="height" priority="250" id="Y9P-Ek-wjg"/> <constraint firstAttribute="height" priority="250" id="Y9P-Ek-wjg"/>
</constraints> </constraints>
</stackView> </stackView>
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="BGD-sd-SQR"> <tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="BGD-sd-SQR">
<rect key="frame" x="0.0" y="0.0" width="375" height="626"/> <rect key="frame" x="0.0" y="20" width="375" height="606"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="RoomVCBubblesTableView"/> <userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="RoomVCBubblesTableView"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableView> </tableView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="54r-18-K1g"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="54r-18-K1g">
<rect key="frame" x="0.0" y="0.0" width="375" height="368"/> <rect key="frame" x="0.0" y="20" width="375" height="368"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="RoomVCPreviewHeaderContainer"/> <accessibility key="accessibilityConfiguration" identifier="RoomVCPreviewHeaderContainer"/>
<constraints> <constraints>
@ -69,7 +68,7 @@
</constraints> </constraints>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="fmF-ad-erE"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="fmF-ad-erE">
<rect key="frame" x="0.0" y="0.0" width="375" height="0.0"/> <rect key="frame" x="0.0" y="20" width="375" height="0.0"/>
<subviews> <subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hB3-nR-MVR"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hB3-nR-MVR">
<rect key="frame" x="0.0" y="0.0" width="375" height="54"/> <rect key="frame" x="0.0" y="0.0" width="375" height="54"/>
@ -189,7 +188,7 @@
</constraints> </constraints>
</view> </view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gt1-EO-UVY"> <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gt1-EO-UVY">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="20" width="375" height="647"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view> </view>
</subviews> </subviews>
@ -237,11 +236,6 @@
<point key="canvasLocation" x="136.80000000000001" y="152.47376311844079"/> <point key="canvasLocation" x="136.80000000000001" y="152.47376311844079"/>
</view> </view>
</objects> </objects>
<designables>
<designable name="QHs-rM-UU8">
<size key="intrinsicContentSize" width="7.5" height="13.5"/>
</designable>
</designables>
<resources> <resources>
<image name="new_close" width="16" height="16"/> <image name="new_close" width="16" height="16"/>
<image name="room_scroll_up" width="24" height="24"/> <image name="room_scroll_up" width="24" height="24"/>

View file

@ -164,6 +164,11 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell {
roomCellContentView.didTapAddParticipants = { [weak self] in roomCellContentView.didTapAddParticipants = { [weak self] in
self?.notifyDelegate(with: RoomCreationIntroCell.tapOnAddParticipants) self?.notifyDelegate(with: RoomCreationIntroCell.tapOnAddParticipants)
} }
self.accessibilityElements = [roomCellContentView.roomAvatarView as Any,
roomCellContentView.titleLabel as Any,
roomCellContentView.informationLabel as Any,
roomCellContentView.addParticipantsContainerView as Any]
} }

View file

@ -69,8 +69,10 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable {
self.addParticipantsButton.layer.masksToBounds = true self.addParticipantsButton.layer.masksToBounds = true
self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside) self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside)
self.addParticipantsButton.accessibilityLabel = VectorL10n.roomIntroCellAddParticipantsAction
self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction
self.addParticipantsLabel.isAccessibilityElement = false
self.roomAvatarView.showCameraBadgeOnFallbackImage = true self.roomAvatarView.showCameraBadgeOnFallbackImage = true
} }

View file

@ -35,6 +35,15 @@ class TextMessageOutgoingWithoutSenderInfoBubbleCell: TextMessageBaseBubbleCell,
self.textMessageContentView?.bubbleBackgroundView?.backgroundColor = theme.roomCellOutgoingBubbleBackgroundColor self.textMessageContentView?.bubbleBackgroundView?.backgroundColor = theme.roomCellOutgoingBubbleBackgroundColor
} }
override func render(_ cellData: MXKCellData!) {
// This cell displays an outgoing message without any sender information.
// However, we need to set the following properties to our cellData, otherwise, to make room for the timestamp, a whitespace could be added when calculating the position of the components.
// If we don't, the component frame calculation will not work for this cell.
(cellData as? RoomBubbleCellData)?.shouldHideSenderName = false
(cellData as? RoomBubbleCellData)?.shouldHideSenderInformation = false
super.render(cellData)
}
// MARK: - Private // MARK: - Private
private func setupBubbleConstraints() { private func setupBubbleConstraints() {

View file

@ -23,6 +23,7 @@
class RoomInputToolbarTextView: UITextView { class RoomInputToolbarTextView: UITextView {
private var heightConstraint: NSLayoutConstraint! private var heightConstraint: NSLayoutConstraint!
private var pillViews = [UIView]()
weak var toolbarDelegate: RoomInputToolbarTextViewDelegate? weak var toolbarDelegate: RoomInputToolbarTextViewDelegate?
@ -51,12 +52,18 @@ class RoomInputToolbarTextView: UITextView {
} }
override var text: String! { override var text: String! {
willSet {
flushPills()
}
didSet { didSet {
updateUI() updateUI()
} }
} }
override var attributedText: NSAttributedString! { override var attributedText: NSAttributedString! {
willSet {
flushPills()
}
didSet { didSet {
updateUI() updateUI()
} }
@ -162,3 +169,17 @@ class RoomInputToolbarTextView: UITextView {
delegate.onTouchUp(inside: delegate.rightInputToolbarButton) delegate.onTouchUp(inside: delegate.rightInputToolbarButton)
} }
} }
extension RoomInputToolbarTextView: PillViewFlusher {
func registerPillView(_ pillView: UIView) {
pillViews.append(pillView)
}
private func flushPills() {
for view in pillViews {
view.alpha = 0.0
view.removeFromSuperview()
}
pillViews.removeAll()
}
}

View file

@ -22,7 +22,7 @@
@class RoomInputToolbarView; @class RoomInputToolbarView;
@class LinkActionWrapper; @class LinkActionWrapper;
@class SuggestionPatternWrapper; @class SuggestionPatternWrapper;
@class UserSuggestionViewModelContextWrapper; @class CompletionSuggestionViewModelContextWrapper;
/** /**
Destination of the message in the composer Destination of the message in the composer
@ -84,7 +84,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
- (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern;
- (UserSuggestionViewModelContextWrapper *)userSuggestionContext; - (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext;
- (MXMediaManager *)mediaManager; - (MXMediaManager *)mediaManager;

View file

@ -70,6 +70,8 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
_sendMode = RoomInputToolbarViewSendModeSend; _sendMode = RoomInputToolbarViewSendModeSend;
self.inputContextViewHeightConstraint.constant = 0; self.inputContextViewHeightConstraint.constant = 0;
self.inputContextLabel.isAccessibilityElement = NO;
self.inputContextButton.isAccessibilityElement = NO;
[self.rightInputToolbarButton setTitle:nil forState:UIControlStateNormal]; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateNormal];
[self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted]; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted];
@ -252,6 +254,10 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
break; break;
} }
// Hide the context items from VoiceOver when the context view is "hidden".
self.inputContextLabel.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0;
self.inputContextButton.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0;
[self.rightInputToolbarButton setImage:buttonImage forState:UIControlStateNormal]; [self.rightInputToolbarButton setImage:buttonImage forState:UIControlStateNormal];
if (self.maxHeight && updatedHeight > self.maxHeight) if (self.maxHeight && updatedHeight > self.maxHeight)
@ -477,11 +483,22 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
[self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor], [self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor],
[self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor], [self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor],
[self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]]; [self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]];
// The voice message toolbar is taller than the input toolbar so the record button is read
// out before the other subviews. Fix this by manually adding the elements in the right order.
self.accessibilityElements = @[self.attachMediaButton,
self.actionsBar,
self.inputContextLabel,
self.inputContextButton,
self.textView,
self.rightInputToolbarButton,
self.voiceMessageToolbarView];
} }
else else
{ {
[self.voiceMessageToolbarView removeFromSuperview]; [self.voiceMessageToolbarView removeFromSuperview];
_voiceMessageToolbarView = nil; _voiceMessageToolbarView = nil;
self.accessibilityElements = nil;
} }
} }
@end @end

View file

@ -96,11 +96,17 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
// Note: this is only interactive in plain text mode. If RTE is enabled, // Note: this is only interactive in plain text mode. If RTE is enabled,
// APIs from the composer view model should be used. // APIs from the composer view model should be used.
get { get {
guard !self.textFormattingEnabled else { return nil } guard !self.textFormattingEnabled else {
MXLog.failure("[WysiwygInputToolbarView] Trying to get attributedTextMessage in RTE mode")
return nil
}
return self.wysiwygViewModel.textView.attributedText return self.wysiwygViewModel.textView.attributedText
} }
set { set {
guard !self.textFormattingEnabled else { return } guard !self.textFormattingEnabled else {
MXLog.failure("[WysiwygInputToolbarView] Trying to set attributedTextMessage in RTE mode")
return
}
self.wysiwygViewModel.textView.attributedText = newValue self.wysiwygViewModel.textView.attributedText = newValue
} }
} }
@ -175,6 +181,16 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
} }
} }
override func setPartialContent(_ attributedTextMessage: NSAttributedString) {
let content: String
if #available(iOS 15.0, *) {
content = PillsFormatter.stringByReplacingPills(in: attributedTextMessage, mode: .markdown)
} else {
content = attributedTextMessage.string
}
self.wysiwygViewModel.setMarkdownContent(content)
}
func showKeyboard() { func showKeyboard() {
self.viewModel.showKeyboard() self.viewModel.showKeyboard()
} }
@ -191,11 +207,15 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
} }
func mention(_ member: MXRoomMember) { func mention(_ member: MXRoomMember) {
self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), self.wysiwygViewModel.setMention(url: MXTools.permalinkToUser(withUserId: member.userId),
name: member.displayname, name: member.displayname,
mentionType: .user) mentionType: .user)
} }
func command(_ command: String) {
self.wysiwygViewModel.setCommand(name: command)
}
// MARK: - Private // MARK: - Private
private func setupComposerIfNeeded() { private func setupComposerIfNeeded() {
@ -219,7 +239,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
let composer = Composer( let composer = Composer(
viewModel: viewModel.context, viewModel: viewModel.context,
wysiwygViewModel: wysiwygViewModel, wysiwygViewModel: wysiwygViewModel,
userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context, completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context,
resizeAnimationDuration: Double(kResizeComposerAnimationDuration), resizeAnimationDuration: Double(kResizeComposerAnimationDuration),
sendMessageAction: { [weak self] content in sendMessageAction: { [weak self] content in
guard let self = self else { return } guard let self = self else { return }
@ -277,12 +297,31 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
}, },
wysiwygViewModel.$plainTextContent wysiwygViewModel.$plainTextContent
.dropFirst()
.removeDuplicates() .removeDuplicates()
.sink { [weak self] value in .dropFirst()
guard let self else { return } .sink { [weak self] attributed in
self.textMessage = value.string // Note: filter out `plainTextMode` being off, as switching to RTE will trigger this
// publisher with empty content. This avoids saving the partial text message
// or trying to compute suggestion from this empty content.
guard let self, self.wysiwygViewModel.plainTextMode else { return }
self.textMessage = attributed.string
self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self)
self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed)
},
wysiwygViewModel.$attributedContent
.removeDuplicates(by: {
$0.text == $1.text
})
.dropFirst()
.sink { [weak self] _ in
// Note: filter out `plainTextMode` being on, as switching to plain text mode will trigger this
// publisher with empty content. This avoids saving the partial text message
// or trying to compute suggestion from this empty content.
guard let self, !self.wysiwygViewModel.plainTextMode else { return }
let markdown = self.wysiwygViewModel.content.markdown
let attributed = NSAttributedString(string: markdown, attributes: [.font: self.defaultFont])
self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed)
} }
] ]
@ -334,7 +373,30 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
} }
private func sendWysiwygMessage(content: WysiwygComposerContent) { private func sendWysiwygMessage(content: WysiwygComposerContent) {
if content.markdown.prefix(while: { $0 == "/" }).count == 1 {
let commandText: String
if content.markdown.hasPrefix(MXKSlashCommand.emote.cmd) {
// `/me` command works with markdown content
commandText = content.markdown
} else if #available(iOS 15.0, *) {
// Other commands should see pills replaced by matrix identifiers
commandText = PillsFormatter.stringByReplacingPills(in: self.wysiwygViewModel.textView.attributedText, mode: .identifier)
} else {
// Without Pills support, just use the raw text for command
commandText = self.wysiwygViewModel.textView.text
}
// Fix potential command failures due to trailing characters
// or NBSP that are not properly handled by the command interpreter
let sanitizedCommand = commandText
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: String.nbsp, with: " ")
delegate?.roomInputToolbarView?(self, sendCommand: sanitizedCommand)
} else {
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown)
}
if isMaximised { if isMaximised {
minimise() minimise()
} }

View file

@ -1,62 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import <MatrixSDK/MatrixSDK.h>
/**
The `RoomKeyRequestViewController` display a modal dialog at the top of the
application asking the user if he wants to share room keys with a user's device.
For the moment, the user is himself.
*/
@interface RoomKeyRequestViewController : NSObject
/**
The UIAlertController instance which handles the dialog.
*/
@property (nonatomic, readonly) UIAlertController *alertController;
@property (nonatomic, readonly) MXSession *mxSession;
@property (nonatomic, readonly) MXDeviceInfo *device;
/**
Initialise an `RoomKeyRequestViewController` instance.
@param deviceInfo the device to share keys to.
@param wasNewDevice flag indicating whether this is the first time we meet the device.
@param session the related matrix session.
@param crypto the related (legacy) crypto module
@param onComplete a block called when the the dialog is closed.
@return the newly created instance.
*/
- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo
wasNewDevice:(BOOL)wasNewDevice
andMatrixSession:(MXSession*)session
crypto:(MXLegacyCrypto *)crypto
onComplete:(void (^)(void))onComplete;
/**
Show the dialog in a modal way.
*/
- (void)show;
/**
Hide the dialog.
*/
- (void)hide;
@end

View file

@ -1,195 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "RoomKeyRequestViewController.h"
#import "GeneratedInterface-Swift.h"
@interface RoomKeyRequestViewController () <KeyVerificationCoordinatorBridgePresenterDelegate>
{
void (^onComplete)(void);
KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter;
BOOL wasNewDevice;
}
@property (nonatomic, strong) MXLegacyCrypto *crypto;
@end
@implementation RoomKeyRequestViewController
- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo
wasNewDevice:(BOOL)theWasNewDevice
andMatrixSession:(MXSession *)session
crypto:(MXLegacyCrypto *)crypto
onComplete:(void (^)(void))onCompleteBlock
{
self = [super init];
if (self)
{
_mxSession = session;
_crypto = crypto;
_device = deviceInfo;
wasNewDevice = theWasNewDevice;
onComplete = onCompleteBlock;
}
return self;
}
- (void)show
{
// Show it modally on the root view controller
UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController;
if (rootViewController)
{
NSString *title = [VectorL10n e2eRoomKeyRequestTitle];
NSString *message;
if (wasNewDevice)
{
message = [VectorL10n e2eRoomKeyRequestMessageNewDevice:_device.displayName];
}
else
{
message = [VectorL10n e2eRoomKeyRequestMessage:_device.displayName];
}
_alertController = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
__weak typeof(self) weakSelf = self;
[_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestStartVerification]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->_alertController = nil;
[self showVerificationView];
}
}]];
[_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestShareWithoutVerifying]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->_alertController = nil;
// Accept the received requests from this device
[self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
}
}]];
[_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestIgnoreRequest]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->_alertController = nil;
// Ignore all pending requests from this device
[self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
}
}]];
[rootViewController presentViewController:_alertController animated:YES completion:nil];
}
}
- (void)hide
{
if (_alertController)
{
[_alertController dismissViewControllerAnimated:YES completion:nil];
_alertController = nil;
}
}
- (void)showVerificationView
{
// Show it modally on the root view controller
UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController;
if (rootViewController)
{
keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:_mxSession];
keyVerificationCoordinatorBridgePresenter.delegate = self;
[keyVerificationCoordinatorBridgePresenter presentFrom:rootViewController otherUserId:_device.userId otherDeviceId:_device.deviceId animated:YES];
}
}
#pragma mark - DeviceVerificationCoordinatorBridgePresenterDelegate
- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId
{
[self dismissKeyVerificationCoordinatorBridgePresenter];
}
- (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter
{
[self dismissKeyVerificationCoordinatorBridgePresenter];
}
- (void)dismissKeyVerificationCoordinatorBridgePresenter
{
[keyVerificationCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
keyVerificationCoordinatorBridgePresenter = nil;
// Check device new status
[self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap<MXDeviceInfo *> *usersDevicesInfoMap, NSDictionary<NSString *,MXCrossSigningInfo *> *crossSigningKeysMap) {
MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId];
if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified)
{
// Accept the received requests from this device
// As the device is now verified, all other key requests will be automatically accepted.
[self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
}
else
{
// Come back to self.alertController - ie, reopen it
[self show];
}
} failure:^(NSError *error) {
// Should not happen (the device is in the crypto db)
[self show];
}];
}
@end

View file

@ -121,11 +121,12 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
private func showSecureBackupSetup(checkKeyBackup: Bool) { private func showSecureBackupSetup(checkKeyBackup: Bool) {
let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable) let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable)
coordinator.delegate = self coordinator.delegate = self
coordinator.start() // Fix: calling coordinator.start() will update the navigationRouter without a popCompletion
coordinator.start(popCompletion: { [weak self] in
self.navigationRouter.push(coordinator.toPresentable(), animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: coordinator) self?.remove(childCoordinator: coordinator)
}) })
// Fix: do not push the presentable from the coordinator to the navigation router as this has already been done by coordinator.start().
// Also, coordinator.toPresentable() returns a navigation controller, which cannot be pushed into a navigation router.
self.add(childCoordinator: coordinator) self.add(childCoordinator: coordinator)
} }
} }

View file

@ -94,11 +94,11 @@ extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate {
extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate { extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate {
func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) {
self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:])) self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:]))
} }
func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) {
self.secretsResetViewModel.process(viewAction: .authenticationCancelled)
self.remove(childCoordinator: coordinator) self.remove(childCoordinator: coordinator)
} }

View file

@ -22,6 +22,7 @@ import Foundation
enum SecretsResetViewAction { enum SecretsResetViewAction {
case loadData case loadData
case reset case reset
case authenticationCancelled
case authenticationInfoEntered(_ authInfo: [String: Any]) case authenticationInfoEntered(_ authInfo: [String: Any])
case cancel case cancel
} }

View file

@ -132,6 +132,8 @@ final class SecretsResetViewController: UIViewController {
self.renderLoading() self.renderLoading()
case .resetDone: case .resetDone:
self.renderLoaded() self.renderLoaded()
case .resetCancelled:
self.renderCancelled()
case .error(let error): case .error(let error):
self.render(error: error) self.render(error: error)
} }
@ -145,6 +147,10 @@ final class SecretsResetViewController: UIViewController {
self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.activityPresenter.removeCurrentActivityIndicator(animated: true)
} }
private func renderCancelled() {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
}
private func render(error: Error) { private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)

View file

@ -49,6 +49,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
break break
case .reset: case .reset:
self.askAuthentication() self.askAuthentication()
case .authenticationCancelled:
self.authenticationCancelled()
case .authenticationInfoEntered(let authParameters): case .authenticationInfoEntered(let authParameters):
self.resetSecrets(with: authParameters) self.resetSecrets(with: authParameters)
case .cancel: case .cancel:
@ -68,7 +70,6 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
} }
MXLog.debug("[SecretsResetViewModel] resetSecrets") MXLog.debug("[SecretsResetViewModel] resetSecrets")
self.update(viewState: .resetting)
crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in
guard let self = self else { guard let self = self else {
return return
@ -96,7 +97,13 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
} }
private func askAuthentication() { private func askAuthentication() {
self.update(viewState: .resetting)
let setupCrossSigningRequest = self.crossSigningService.setupCrossSigningRequest() let setupCrossSigningRequest = self.crossSigningService.setupCrossSigningRequest()
self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: setupCrossSigningRequest) self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: setupCrossSigningRequest)
} }
private func authenticationCancelled() {
self.update(viewState: .resetCancelled)
}
} }

View file

@ -22,5 +22,6 @@ import Foundation
enum SecretsResetViewState { enum SecretsResetViewState {
case resetting case resetting
case resetDone case resetDone
case resetCancelled
case error(Error) case error(Error)
} }

View file

@ -73,12 +73,16 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
// MARK: - Public methods // MARK: - Public methods
func start() { func start() {
start(popCompletion: nil)
}
func start(popCompletion: (() -> Void)?) {
let rootViewController = self.createIntro() let rootViewController = self.createIntro()
if self.navigationRouter.modules.isEmpty == false { if self.navigationRouter.modules.isEmpty == false {
self.navigationRouter.push(rootViewController, animated: true, popCompletion: nil) self.navigationRouter.push(rootViewController, animated: true, popCompletion: popCompletion)
} else { } else {
self.navigationRouter.setRootModule(rootViewController) self.navigationRouter.setRootModule(rootViewController, popCompletion: popCompletion)
} }
} }

View file

@ -1,26 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Enter Pin Code View Controller--> <!--Enter Pin Code View Controller-->
<scene sceneID="mt5-wz-YKA"> <scene sceneID="mt5-wz-YKA">
<objects> <objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="EnterPinCodeViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController"> <viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="EnterPinCodeViewController" customModule="Element" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo"> <view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1YE-D1-eHn"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1YE-D1-eHn">
<rect key="frame" x="0.0" y="20" width="375" height="627"/> <rect key="frame" x="0.0" y="40" width="375" height="607"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="app_symbol" translatesAutoresizingMaskIntoConstraints="NO" id="8qz-Yk-9a4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="app_symbol" translatesAutoresizingMaskIntoConstraints="NO" id="8qz-Yk-9a4">
<rect key="frame" x="137.5" y="243.5" width="100" height="100"/> <rect key="frame" x="137.5" y="233.5" width="100" height="100"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="100" id="Ux9-pH-SW9"/> <constraint firstAttribute="width" constant="100" id="Ux9-pH-SW9"/>
<constraint firstAttribute="height" constant="100" id="o3K-YH-5tn"/> <constraint firstAttribute="height" constant="100" id="o3K-YH-5tn"/>
@ -34,7 +35,7 @@
</constraints> </constraints>
</view> </view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DMT-DS-IA8"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DMT-DS-IA8">
<rect key="frame" x="0.0" y="8" width="375" height="651"/> <rect key="frame" x="0.0" y="28" width="375" height="631"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="app_symbol" translatesAutoresizingMaskIntoConstraints="NO" id="UHg-qE-anw"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="app_symbol" translatesAutoresizingMaskIntoConstraints="NO" id="UHg-qE-anw">
<rect key="frame" x="167.5" y="0.0" width="40" height="40"/> <rect key="frame" x="167.5" y="0.0" width="40" height="40"/>
@ -44,20 +45,20 @@
</constraints> </constraints>
</imageView> </imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Welcome back.Choose a PIN for security" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bxI-mu-qng"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Welcome back.Choose a PIN for security" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bxI-mu-qng">
<rect key="frame" x="16" y="59" width="343" height="53"/> <rect key="frame" x="16" y="56" width="343" height="53"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="22"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="22"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rn2-qe-htS"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rn2-qe-htS">
<rect key="frame" x="16" y="131.5" width="343" height="54"/> <rect key="frame" x="16" y="124.5" width="343" height="54"/>
<string key="text">Setting up a PIN lets you protect data like messages and contacts, so only you can access them by entering the PIN at the start of the app.</string> <string key="text">Setting up a PIN lets you protect data like messages and contacts, so only you can access them by entering the PIN at the start of the app.</string>
<fontDescription key="fontDescription" type="system" pointSize="15"/> <fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="l5x-qO-sdf"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="l5x-qO-sdf">
<rect key="frame" x="2" y="204.5" width="371" height="79"/> <rect key="frame" x="2" y="194.5" width="371" height="79"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="xi9-P9-8WP"> <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="xi9-P9-8WP">
<rect key="frame" x="101.5" y="0.0" width="168" height="24"/> <rect key="frame" x="101.5" y="0.0" width="168" height="24"/>
@ -97,7 +98,7 @@
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Q0w-RD-JD3"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Q0w-RD-JD3">
<rect key="frame" x="0.0" y="8" width="371" height="2"/> <rect key="frame" x="0.0" y="8" width="371" height="2"/>
<color key="backgroundColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" systemColor="systemRedColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="2" id="thx-rI-kOC"/> <constraint firstAttribute="height" constant="2" id="thx-rI-kOC"/>
</constraints> </constraints>
@ -106,7 +107,7 @@
<rect key="frame" x="8" y="18" width="355" height="29"/> <rect key="frame" x="8" y="18" width="355" height="29"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/> <fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" systemColor="systemRedColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
</subviews> </subviews>
@ -124,12 +125,12 @@
</subviews> </subviews>
</stackView> </stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" alignment="center" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="W0M-eq-abZ"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" alignment="center" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="W0M-eq-abZ">
<rect key="frame" x="77.5" y="302.5" width="220" height="276"/> <rect key="frame" x="77.5" y="289.5" width="220" height="276"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Uqh-o2-7HP"> <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Uqh-o2-7HP">
<rect key="frame" x="0.0" y="0.0" width="220" height="60"/> <rect key="frame" x="0.0" y="0.0" width="220" height="60"/>
<subviews> <subviews>
<button opaque="NO" tag="1" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BEe-II-qt8"> <button opaque="NO" tag="1" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BEe-II-qt8">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/> <rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="HSC-fC-0mb"/> <constraint firstAttribute="width" constant="60" id="HSC-fC-0mb"/>
@ -141,7 +142,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="bJg-z4-FSc"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="bJg-z4-FSc"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="2" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vRL-nn-bIH"> <button opaque="NO" tag="2" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vRL-nn-bIH">
<rect key="frame" x="80" y="0.0" width="60" height="60"/> <rect key="frame" x="80" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="HaJ-0E-bl3"/> <constraint firstAttribute="height" constant="60" id="HaJ-0E-bl3"/>
@ -153,7 +154,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="jWB-aT-7CT"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="jWB-aT-7CT"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="3" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="m5E-4b-a2B"> <button opaque="NO" tag="3" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="m5E-4b-a2B">
<rect key="frame" x="160" y="0.0" width="60" height="60"/> <rect key="frame" x="160" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="9Uy-xG-5Vq"/> <constraint firstAttribute="height" constant="60" id="9Uy-xG-5Vq"/>
@ -170,7 +171,7 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Z9D-6N-neq"> <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Z9D-6N-neq">
<rect key="frame" x="0.0" y="72" width="220" height="60"/> <rect key="frame" x="0.0" y="72" width="220" height="60"/>
<subviews> <subviews>
<button opaque="NO" tag="4" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q2U-ek-vSc"> <button opaque="NO" tag="4" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q2U-ek-vSc">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/> <rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="G0j-6T-fRk"/> <constraint firstAttribute="width" constant="60" id="G0j-6T-fRk"/>
@ -182,7 +183,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="aHq-oD-jeB"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="aHq-oD-jeB"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="5" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ziv-SY-nXQ"> <button opaque="NO" tag="5" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ziv-SY-nXQ">
<rect key="frame" x="80" y="0.0" width="60" height="60"/> <rect key="frame" x="80" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="5Ry-eq-V0D"/> <constraint firstAttribute="width" constant="60" id="5Ry-eq-V0D"/>
@ -194,7 +195,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="t2w-kA-5Ej"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="t2w-kA-5Ej"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="6" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PNU-iI-oCX"> <button opaque="NO" tag="6" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PNU-iI-oCX">
<rect key="frame" x="160" y="0.0" width="60" height="60"/> <rect key="frame" x="160" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="odD-FC-4eB"/> <constraint firstAttribute="width" constant="60" id="odD-FC-4eB"/>
@ -211,7 +212,7 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="YeU-UN-Uo0"> <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="YeU-UN-Uo0">
<rect key="frame" x="0.0" y="144" width="220" height="60"/> <rect key="frame" x="0.0" y="144" width="220" height="60"/>
<subviews> <subviews>
<button opaque="NO" tag="7" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Lnz-5u-oFb"> <button opaque="NO" tag="7" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Lnz-5u-oFb">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/> <rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="Ye8-5w-NMv"/> <constraint firstAttribute="width" constant="60" id="Ye8-5w-NMv"/>
@ -223,7 +224,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="i3U-c4-Cp9"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="i3U-c4-Cp9"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="8" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OCE-R0-CMN"> <button opaque="NO" tag="8" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OCE-R0-CMN">
<rect key="frame" x="80" y="0.0" width="60" height="60"/> <rect key="frame" x="80" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="5LE-yh-yoi"/> <constraint firstAttribute="width" constant="60" id="5LE-yh-yoi"/>
@ -235,7 +236,7 @@
<action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="wpv-qG-N2w"/> <action selector="digitButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="wpv-qG-N2w"/>
</connections> </connections>
</button> </button>
<button opaque="NO" tag="9" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1dz-Qd-zCl"> <button opaque="NO" tag="9" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1dz-Qd-zCl">
<rect key="frame" x="160" y="0.0" width="60" height="60"/> <rect key="frame" x="160" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="rS1-XX-Xw9"/> <constraint firstAttribute="width" constant="60" id="rS1-XX-Xw9"/>
@ -252,15 +253,18 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Nrp-tS-u1k"> <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Nrp-tS-u1k">
<rect key="frame" x="0.0" y="216" width="220" height="60"/> <rect key="frame" x="0.0" y="216" width="220" height="60"/>
<subviews> <subviews>
<button opaque="NO" userInteractionEnabled="NO" tag="-99" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DEv-rc-fGB"> <button opaque="NO" userInteractionEnabled="NO" tag="-99" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DEv-rc-fGB">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/> <rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="NO"/>
</accessibility>
<constraints> <constraints>
<constraint firstAttribute="width" constant="60" id="fVd-IS-maA"/> <constraint firstAttribute="width" constant="60" id="fVd-IS-maA"/>
<constraint firstAttribute="height" constant="60" id="x1Z-Ar-22U"/> <constraint firstAttribute="height" constant="60" id="x1Z-Ar-22U"/>
</constraints> </constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sc1-u3-yvh"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sc1-u3-yvh">
<rect key="frame" x="80" y="0.0" width="60" height="60"/> <rect key="frame" x="80" y="0.0" width="60" height="60"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="60" id="DDy-yf-FOR"/> <constraint firstAttribute="height" constant="60" id="DDy-yf-FOR"/>
@ -288,8 +292,8 @@
</stackView> </stackView>
</subviews> </subviews>
</stackView> </stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CRt-Fb-0Dq"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CRt-Fb-0Dq">
<rect key="frame" x="146.5" y="598" width="82" height="33"/> <rect key="frame" x="146.5" y="581" width="82" height="33"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Forgot PIN"/> <state key="normal" title="Forgot PIN"/>
<connections> <connections>
@ -297,7 +301,7 @@
</connections> </connections>
</button> </button>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N9V-f7-d5k"> <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N9V-f7-d5k">
<rect key="frame" x="67.5" y="650" width="240" height="1"/> <rect key="frame" x="67.5" y="630" width="240" height="1"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="1" id="oFX-h1-fx7"/> <constraint firstAttribute="height" constant="1" id="oFX-h1-fx7"/>
@ -312,6 +316,7 @@
</constraints> </constraints>
</stackView> </stackView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/> <color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints> <constraints>
<constraint firstItem="bFg-jh-JZB" firstAttribute="bottom" secondItem="1YE-D1-eHn" secondAttribute="bottom" constant="20" id="5mk-pT-EGS"/> <constraint firstItem="bFg-jh-JZB" firstAttribute="bottom" secondItem="1YE-D1-eHn" secondAttribute="bottom" constant="20" id="5mk-pT-EGS"/>
@ -323,7 +328,6 @@
<constraint firstItem="DMT-DS-IA8" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="kA7-cw-VK1"/> <constraint firstItem="DMT-DS-IA8" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="kA7-cw-VK1"/>
<constraint firstItem="1YE-D1-eHn" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="qOT-0t-zQs"/> <constraint firstItem="1YE-D1-eHn" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="qOT-0t-zQs"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
</view> </view>
<connections> <connections>
<outlet property="bottomView" destination="N9V-f7-d5k" id="H5X-Px-d4w"/> <outlet property="bottomView" destination="N9V-f7-d5k" id="H5X-Px-d4w"/>
@ -350,5 +354,8 @@
<image name="app_symbol" width="120" height="120"/> <image name="app_symbol" width="120" height="120"/>
<image name="back_icon" width="14" height="23"/> <image name="back_icon" width="14" height="23"/>
<image name="selection_untick" width="22" height="22"/> <image name="selection_untick" width="22" height="22"/>
<systemColor name="systemRedColor">
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources> </resources>
</document> </document>

View file

@ -91,6 +91,10 @@ final class SetPinCoordinatorBridgePresenter: NSObject {
} }
func presentWithMainAppWindow(_ window: UIWindow) { func presentWithMainAppWindow(_ window: UIWindow) {
// Prevents the VoiceOver reading accessible content when the PIN screen is on top
// Calling `makeKeyAndVisible` in `dismissWithMainAppWindow(_:)` restores the visibility state.
window.isHidden = true
let pinCoordinatorWindow = UIWindow(frame: window.bounds) let pinCoordinatorWindow = UIWindow(frame: window.bounds)
let setPinCoordinator = SetPinCoordinator(session: self.session, viewMode: self.viewMode, pinCodePreferences: .shared) let setPinCoordinator = SetPinCoordinator(session: self.session, viewMode: self.viewMode, pinCodePreferences: .shared)

View file

@ -176,8 +176,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
LABS_ENABLE_NEW_SESSION_MANAGER, LABS_ENABLE_NEW_SESSION_MANAGER,
LABS_ENABLE_NEW_CLIENT_INFO_FEATURE, LABS_ENABLE_NEW_CLIENT_INFO_FEATURE,
LABS_ENABLE_WYSIWYG_COMPOSER, LABS_ENABLE_WYSIWYG_COMPOSER,
LABS_ENABLE_VOICE_BROADCAST, LABS_ENABLE_VOICE_BROADCAST
LABS_ENABLE_CRYPTO_SDK
}; };
typedef NS_ENUM(NSUInteger, SECURITY) typedef NS_ENUM(NSUInteger, SECURITY)
@ -588,11 +587,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
if (BuildSettings.settingsScreenShowLabSettings) if (BuildSettings.settingsScreenShowLabSettings)
{ {
Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS];
if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId])
{
[sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK];
}
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS];
@ -2587,18 +2581,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside]; [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
else if (row == LABS_ENABLE_CRYPTO_SDK)
{
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK;
labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk;
labelAndSwitchCell.mxkSwitch.on = isEnabled;
[labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled];
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell; cell = labelAndSwitchCell;
} }
} }
@ -3372,30 +3354,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
RiotSettings.shared.enableVoiceBroadcast = sender.isOn; RiotSettings.shared.enableVoiceBroadcast = sender.isOn;
} }
- (void)enableCryptoSDKFeature:(UISwitch *)sender
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk
message:VectorL10n.settingsLabsConfirmCryptoSdk
preferredStyle:UIAlertControllerStyleAlert];
MXWeakify(self);
[confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
self->currentAlert = nil;
[sender setOn:NO animated:YES];
}]];
[confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[CryptoSDKFeature.shared enable];
[[AppDelegate theDelegate] reloadMatrixSessions:YES];
}]];
[self presentViewController:confirmationAlert animated:YES completion:nil];
currentAlert = confirmationAlert;
}
- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender
{ {
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
@ -4200,6 +4158,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|| (language == nil && [NSBundle mxk_language])) || (language == nil && [NSBundle mxk_language]))
{ {
[NSBundle mxk_setLanguage:language]; [NSBundle mxk_setLanguage:language];
UIApplication.sharedApplication.accessibilityLanguage = language;
// Store user settings // Store user settings
NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;

View file

@ -35,7 +35,7 @@ class ThreadTableViewCell: UITableViewCell {
@IBOutlet private weak var rootMessageAvatarView: UserAvatarView! @IBOutlet private weak var rootMessageAvatarView: UserAvatarView!
@IBOutlet private weak var rootMessageSenderLabel: UILabel! @IBOutlet private weak var rootMessageSenderLabel: UILabel!
@IBOutlet private weak var rootMessageContentLabel: UILabel! @IBOutlet private weak var rootMessageContentTextView: UITextView!
@IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var lastMessageTimeLabel: UILabel!
@IBOutlet private weak var summaryView: ThreadSummaryView! @IBOutlet private weak var summaryView: ThreadSummaryView!
@IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView! @IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView!
@ -61,7 +61,7 @@ class ThreadTableViewCell: UITableViewCell {
if let rootMessageText = model.rootMessageText { if let rootMessageText = model.rootMessageText {
updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor) updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor)
} else { } else {
rootMessageContentLabel.attributedText = nil rootMessageContentTextView.attributedText = nil
} }
lastMessageTimeLabel.text = model.lastMessageTime lastMessageTimeLabel.text = model.lastMessageTime
if let summaryModel = model.summaryModel { if let summaryModel = model.summaryModel {
@ -83,7 +83,7 @@ class ThreadTableViewCell: UITableViewCell {
mutable.addAttributes([ mutable.addAttributes([
.foregroundColor: color .foregroundColor: color
], range: NSRange(location: 0, length: mutable.length)) ], range: NSRange(location: 0, length: mutable.length))
rootMessageContentLabel.attributedText = mutable rootMessageContentTextView.attributedText = mutable
} }
} }
@ -97,7 +97,7 @@ extension ThreadTableViewCell: Themable {
Self.usernameColorGenerator.update(theme: theme) Self.usernameColorGenerator.update(theme: theme)
updateRootMessageSenderColor() updateRootMessageSenderColor()
rootMessageAvatarView.backgroundColor = .clear rootMessageAvatarView.backgroundColor = .clear
if let attributedText = rootMessageContentLabel.attributedText { if let attributedText = rootMessageContentTextView.attributedText {
updateRootMessageContentAttributes(attributedText, color: rootMessageColor) updateRootMessageContentAttributes(attributedText, color: rootMessageColor)
} }
lastMessageTimeLabel.textColor = theme.colors.secondaryContent lastMessageTimeLabel.textColor = theme.colors.secondaryContent

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -11,14 +11,14 @@
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="94" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target"> <tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="94" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Element" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="94"/> <rect key="frame" x="0.0" y="0.0" width="320" height="94"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="94"/> <rect key="frame" x="0.0" y="0.0" width="320" height="94"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I32-A5-WWw" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I32-A5-WWw" customClass="UserAvatarView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="12" y="12" width="32" height="32"/> <rect key="frame" x="12" y="12" width="32" height="32"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
@ -27,7 +27,7 @@
</constraints> </constraints>
</view> </view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
<rect key="frame" x="56" y="12" width="201" height="17"/> <rect key="frame" x="56" y="12" width="201" height="9"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -38,7 +38,7 @@
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<view clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aUq-D2-1KM" customClass="ThreadNotificationStatusView" customModule="Riot" customModuleProvider="target"> <view clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aUq-D2-1KM" customClass="ThreadNotificationStatusView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="302" y="17" width="8" height="8"/> <rect key="frame" x="302" y="17" width="8" height="8"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
@ -51,13 +51,13 @@
</userDefinedRuntimeAttribute> </userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</view> </view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Message" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzR-f9-3qV"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalHuggingPriority="252" verticalCompressionResistancePriority="752" scrollEnabled="NO" text="Message" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ebd-1P-ezA" customClass="MXKMessageTextView">
<rect key="frame" x="56" y="33" width="236" height="17"/> <rect key="frame" x="56" y="21" width="236" height="33"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<nil key="highlightedColor"/> </textView>
</label> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Element" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="44" y="54" width="264" height="32"/> <rect key="frame" x="44" y="54" width="264" height="32"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
@ -68,20 +68,20 @@
<constraints> <constraints>
<constraint firstItem="I32-A5-WWw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="28p-b3-xMJ"/> <constraint firstItem="I32-A5-WWw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="28p-b3-xMJ"/>
<constraint firstItem="108-Xh-aZf" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="2Dt-BH-xjF"/> <constraint firstItem="108-Xh-aZf" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="2Dt-BH-xjF"/>
<constraint firstItem="Md3-uq-cSB" firstAttribute="top" secondItem="xzR-f9-3qV" secondAttribute="bottom" constant="4" id="6mB-Yd-Pyg"/>
<constraint firstAttribute="trailing" secondItem="aUq-D2-1KM" secondAttribute="trailing" constant="10" id="Du2-UR-wBe"/> <constraint firstAttribute="trailing" secondItem="aUq-D2-1KM" secondAttribute="trailing" constant="10" id="Du2-UR-wBe"/>
<constraint firstItem="Md3-uq-cSB" firstAttribute="top" secondItem="ebd-1P-ezA" secondAttribute="bottom" id="Gid-IZ-H8n"/>
<constraint firstAttribute="bottom" secondItem="Md3-uq-cSB" secondAttribute="bottom" constant="8" id="Ppd-HN-Ehg"/> <constraint firstAttribute="bottom" secondItem="Md3-uq-cSB" secondAttribute="bottom" constant="8" id="Ppd-HN-Ehg"/>
<constraint firstItem="I32-A5-WWw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="Trt-CK-Tly"/> <constraint firstItem="I32-A5-WWw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="Trt-CK-Tly"/>
<constraint firstItem="Md3-uq-cSB" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="44" id="Vpf-02-TgV"/> <constraint firstItem="Md3-uq-cSB" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="44" id="Vpf-02-TgV"/>
<constraint firstAttribute="trailing" secondItem="xzR-f9-3qV" secondAttribute="trailing" constant="28" id="Zz9-PK-l9b"/>
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="leading" secondItem="108-Xh-aZf" secondAttribute="trailing" constant="8" id="bE8-Yy-3B9"/> <constraint firstItem="C2U-Ih-4Oh" firstAttribute="leading" secondItem="108-Xh-aZf" secondAttribute="trailing" constant="8" id="bE8-Yy-3B9"/>
<constraint firstItem="xzR-f9-3qV" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="g8i-lt-K8f"/> <constraint firstAttribute="trailing" secondItem="ebd-1P-ezA" secondAttribute="trailing" constant="28" id="pIa-CD-yiY"/>
<constraint firstItem="aUq-D2-1KM" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="17" id="rvj-qg-S3J"/> <constraint firstItem="aUq-D2-1KM" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="17" id="rvj-qg-S3J"/>
<constraint firstItem="108-Xh-aZf" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="sXf-FI-gD3"/> <constraint firstItem="108-Xh-aZf" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="sXf-FI-gD3"/>
<constraint firstItem="xzR-f9-3qV" firstAttribute="top" secondItem="108-Xh-aZf" secondAttribute="bottom" constant="4" id="tQN-Rr-MIS"/>
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="14" id="u3s-nr-avO"/> <constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="14" id="u3s-nr-avO"/>
<constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="12" id="vxt-vD-jy8"/> <constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="12" id="vxt-vD-jy8"/>
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="27" id="wNc-xV-uIR"/> <constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="27" id="wNc-xV-uIR"/>
<constraint firstItem="ebd-1P-ezA" firstAttribute="top" secondItem="108-Xh-aZf" secondAttribute="bottom" id="wpO-jn-bFQ"/>
<constraint firstItem="ebd-1P-ezA" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="y1L-8a-g85"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/> <viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
@ -89,7 +89,7 @@
<outlet property="lastMessageTimeLabel" destination="C2U-Ih-4Oh" id="pf3-df-T65"/> <outlet property="lastMessageTimeLabel" destination="C2U-Ih-4Oh" id="pf3-df-T65"/>
<outlet property="notificationStatusView" destination="aUq-D2-1KM" id="IDB-Yf-weu"/> <outlet property="notificationStatusView" destination="aUq-D2-1KM" id="IDB-Yf-weu"/>
<outlet property="rootMessageAvatarView" destination="I32-A5-WWw" id="zJW-QQ-jsG"/> <outlet property="rootMessageAvatarView" destination="I32-A5-WWw" id="zJW-QQ-jsG"/>
<outlet property="rootMessageContentLabel" destination="xzR-f9-3qV" id="97u-na-8XW"/> <outlet property="rootMessageContentTextView" destination="ebd-1P-ezA" id="Xsl-po-GP9"/>
<outlet property="rootMessageSenderLabel" destination="108-Xh-aZf" id="nUc-qK-UCD"/> <outlet property="rootMessageSenderLabel" destination="108-Xh-aZf" id="nUc-qK-UCD"/>
<outlet property="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/> <outlet property="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/>
</connections> </connections>
@ -97,6 +97,9 @@
</tableViewCell> </tableViewCell>
</objects> </objects>
<resources> <resources>
<systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>

View file

@ -273,22 +273,12 @@
- (IBAction)onDone:(id)sender - (IBAction)onDone:(id)sender
{ {
// Acknowledge the existence of all devices before leaving this screen // Acknowledge the existence of all devices before leaving this screen
[self startActivityIndicator];
if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices");
return;
}
[(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{
[self stopActivityIndicator];
[self dismissViewControllerAnimated:YES completion:nil]; [self dismissViewControllerAnimated:YES completion:nil];
if (self->onCompleteBlock) if (self->onCompleteBlock)
{ {
self->onCompleteBlock(YES); self->onCompleteBlock(YES);
} }
}];
} }
- (IBAction)onCancel:(id)sender - (IBAction)onCancel:(id)sender

View file

@ -18,6 +18,7 @@
#import "RoomBubbleCellData.h" #import "RoomBubbleCellData.h"
#import "MXKRoomBubbleTableViewCell+Riot.h" #import "MXKRoomBubbleTableViewCell+Riot.h"
#import "UserEncryptionTrustLevel.h" #import "UserEncryptionTrustLevel.h"
#import "RoomEncryptionTrustLevel.h"
#import "RoomReactionsViewSizer.h" #import "RoomReactionsViewSizer.h"
#import "RoomEncryptedDataBubbleCell.h" #import "RoomEncryptedDataBubbleCell.h"
#import "LegacyAppDelegate.h" #import "LegacyAppDelegate.h"

View file

@ -573,8 +573,13 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
{ {
// Force the default text color for the last message (cancel highlighted message color) // Force the default text color for the last message (cancel highlighted message color)
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText]; NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText];
[lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor NSRange range = NSMakeRange(0, lastEventDescription.length);
range:NSMakeRange(0, lastEventDescription.length)]; [lastEventDescription addAttribute:NSForegroundColorAttributeName
value:ThemeService.shared.theme.colors.secondaryContent
range:range];
[lastEventDescription addThemeColorNameAttribute:@"secondaryContent" range:range];
[lastEventDescription addThemeIdentifierAttribute];
summary.lastMessage.attributedText = lastEventDescription; summary.lastMessage.attributedText = lastEventDescription;
} }
@ -670,9 +675,11 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
NSAttributedString *attachmentString = nil; NSAttributedString *attachmentString = nil;
UIColor *textColor; UIColor *textColor;
NSString *colorIdentifier;
if (isStoppedVoiceBroadcast) if (isStoppedVoiceBroadcast)
{ {
textColor = ThemeService.shared.theme.textSecondaryColor; textColor = ThemeService.shared.theme.colors.secondaryContent;
colorIdentifier = @"secondaryContent";
NSString *senderDisplayName; NSString *senderDisplayName;
if ([stateEvent.stateKey isEqualToString:session.myUser.userId]) if ([stateEvent.stateKey isEqualToString:session.myUser.userId])
{ {
@ -688,6 +695,7 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
else else
{ {
textColor = ThemeService.shared.theme.colors.alert; textColor = ThemeService.shared.theme.colors.alert;
colorIdentifier = @"alert";
UIImage *liveImage = AssetImages.voiceBroadcastLive.image; UIImage *liveImage = AssetImages.voiceBroadcastLive.image;
NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
@ -717,6 +725,12 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
} }
[lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)]; [lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)];
if (colorIdentifier)
{
[lastMessage addThemeColorNameAttribute:colorIdentifier range:NSMakeRange(0, lastMessage.length)];
[lastMessage addThemeIdentifierAttribute];
}
summary.lastMessage.attributedText = lastMessage; summary.lastMessage.attributedText = lastMessage;
return YES; return YES;

View file

@ -0,0 +1,48 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Utility struct to get a theme color by its name
struct ThemeColorResolver {
private static var theme: Theme?
private static var colorsTable: [String: UIColor] = [:]
private static let queue = DispatchQueue(label: "io.element.ThemeColorResolver.queue", qos: .userInteractive)
private static func setTheme(theme: Theme) {
queue.sync {
guard self.theme?.identifier != theme.identifier else {
return
}
self.theme = theme
colorsTable = [:]
let mirror = Mirror(reflecting: theme.colors)
for child in mirror.children {
if let colorName = child.label {
colorsTable[colorName] = child.value as? UIColor
}
}
}
}
/// Finds a color by its name in the current theme colors
/// - Parameter name: color name
/// - Returns: the corresponding color or nil
static func getColorByName(_ name: String) -> UIColor? {
setTheme(theme: ThemeService.shared().theme)
return colorsTable[name]
}
}

View file

@ -41,7 +41,6 @@ class NotificationService: UNNotificationServiceExtension {
private var ongoingVoIPPushRequests: [String: Bool] = [:] private var ongoingVoIPPushRequests: [String: Bool] = [:]
private var userAccount: MXKAccount? private var userAccount: MXKAccount?
private var isCryptoSDKEnabled = false
/// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's
private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] private var bestAttemptContents: [String: UNMutableNotificationContent] = [:]
@ -196,13 +195,12 @@ class NotificationService: UNNotificationServiceExtension {
self.userAccount = MXKAccountManager.shared()?.activeAccounts.first self.userAccount = MXKAccountManager.shared()?.activeAccounts.first
if let userAccount = userAccount { if let userAccount = userAccount {
Self.backgroundServiceInitQueue.sync { Self.backgroundServiceInitQueue.sync {
if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials {
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE")
self.logMemory() self.logMemory()
NotificationService.backgroundSyncService = MXBackgroundSyncService( NotificationService.backgroundSyncService = MXBackgroundSyncService(
withCredentials: userAccount.mxCredentials, withCredentials: userAccount.mxCredentials,
isCryptoSDKEnabled: isCryptoSDKEnabled,
persistTokenDataHandler: { persistTokenDataHandler in persistTokenDataHandler: { persistTokenDataHandler in
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
}, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in
@ -219,16 +217,6 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
/// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require
/// rebuilding `MXBackgroundSyncService`
private func hasChangedCryptoSDK() -> Bool {
guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else {
return false
}
isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK
return true
}
/// Attempts to preprocess payload and attach room display name to the best attempt content /// Attempts to preprocess payload and attach room display name to the best attempt content
/// - Parameters: /// - Parameters:
/// - eventId: Event identifier to mutate best attempt content /// - eventId: Event identifier to mutate best attempt content

View file

@ -102,11 +102,6 @@ static MXSession *fakeSession;
[session setStore:self.fileStore success:^{ [session setStore:self.fileStore success:^{
MXStrongifyAndReturnIfNil(session); MXStrongifyAndReturnIfNil(session);
if ([session.crypto isKindOfClass:[MXLegacyCrypto class]])
{
((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now
}
self.selectedRooms = [NSMutableArray array]; self.selectedRooms = [NSMutableArray array];
for (NSString *roomIdentifier in roomIdentifiers) { for (NSString *roomIdentifier in roomIdentifiers) {
MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session];

View file

@ -6,6 +6,8 @@
#import "AvatarGenerator.h" #import "AvatarGenerator.h"
#import "BuildInfo.h" #import "BuildInfo.h"
#import "ShareItemSender.h" #import "ShareItemSender.h"
#import "UserEncryptionTrustLevel.h"
#import "RoomEncryptionTrustLevel.h"
// MatrixKit imports // MatrixKit imports
#import "MatrixKit-Bridging-Header.h" #import "MatrixKit-Bridging-Header.h"

View file

@ -87,3 +87,4 @@ targets:
- "**/*.md" # excludes all files with the .md extension - "**/*.md" # excludes all files with the .md extension
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
- path: ../Riot/Modules/Encryption/EncryptionTrustLevel.swift

View file

@ -86,6 +86,10 @@ class HomeserverAddress: NSObject {
/// - Ensure the address contains a scheme, otherwise make it `https`. /// - Ensure the address contains a scheme, otherwise make it `https`.
/// - Remove any trailing slashes. /// - Remove any trailing slashes.
static func sanitized(_ address: String) -> String { static func sanitized(_ address: String) -> String {
guard !address.isEmpty else {
// prevent prefixing an empty string with "https:"
return address
}
var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if !address.contains("://") { if !address.contains("://") {

View file

@ -267,17 +267,6 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)
// MXLog.debug("[QRLoginService] Session created without E2EE support. Inform the interlocutor of finishing")
// guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .success)),
// case .success = await rendezvousService.send(data: requestData) else {
// await teardownRendezvous(state: .failed(error: .rendezvousFailed))
// return
// }
//
// MXLog.debug("[QRLoginService] Login flow finished, returning session")
// state = .completed(session: session, securityCompleted: false)
// return
let cryptoResult = await withCheckedContinuation { continuation in let cryptoResult = await withCheckedContinuation { continuation in
session.enableCrypto(true) { response in session.enableCrypto(true) { response in
continuation.resume(returning: response) continuation.resume(returning: response)

View file

@ -57,7 +57,13 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
self.parameters = parameters self.parameters = parameters
let homeserver = parameters.authenticationService.state.homeserver let homeserver = parameters.authenticationService.state.homeserver
let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.displayableAddress, let homeserverAddress: String
if BuildSettings.forceHomeserverSelection, homeserver.addressFromUser == nil {
homeserverAddress = ""
} else {
homeserverAddress = homeserver.displayableAddress
}
let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserverAddress,
flow: parameters.authenticationService.state.flow, flow: parameters.authenticationService.state.flow,
hasModalPresentation: parameters.hasModalPresentation) hasModalPresentation: parameters.hasModalPresentation)
let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context) let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context)

View file

@ -51,7 +51,7 @@ enum MockAppScreens {
MockStaticLocationViewingScreenState.self, MockStaticLocationViewingScreenState.self,
MockLocationSharingScreenState.self, MockLocationSharingScreenState.self,
MockAnalyticsPromptScreenState.self, MockAnalyticsPromptScreenState.self,
MockUserSuggestionScreenState.self, MockCompletionSuggestionScreenState.self,
MockPollEditFormScreenState.self, MockPollEditFormScreenState.self,
MockSpaceCreationEmailInvitesScreenState.self, MockSpaceCreationEmailInvitesScreenState.self,
MockSpaceSettingsScreenState.self, MockSpaceSettingsScreenState.self,

View file

@ -0,0 +1,43 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum CompletionSuggestionViewAction {
case selectedItem(CompletionSuggestionViewStateItem)
}
enum CompletionSuggestionViewModelResult {
case selectedItemWithIdentifier(String)
}
enum CompletionSuggestionViewStateItem: Identifiable {
case command(name: String, parametersFormat: String, description: String)
case user(id: String, avatar: AvatarInputProtocol?, displayName: String?)
var id: String {
switch self {
case .command(let name, _, _):
return name
case .user(let id, _, _):
return id
}
}
}
struct CompletionSuggestionViewState: BindableState {
var items: [CompletionSuggestionViewStateItem]
}

View file

@ -0,0 +1,95 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
enum MockCompletionSuggestionScreenState: MockScreenState, CaseIterable {
case multipleResults
private static var members: [RoomMembersProviderMember]!
var screenType: Any.Type {
CompletionSuggestionList.self
}
var screenView: ([Any], AnyView) {
let service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self)
let listViewModel = CompletionSuggestionViewModel(completionSuggestionService: service)
let viewModel = CompletionSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
service.processTextMessage(textMessage)
}
return (
[service, listViewModel],
AnyView(CompletionSuggestionListWithInput(viewModel: viewModel)
.environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol {
var canMentionRoom: Bool { false }
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
if Self.members == nil {
Self.members = generateUsersWithCount(10)
}
members(Self.members)
}
private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] {
(0..<count).map { _ in
let identifier = "@" + UUID().uuidString
return RoomMembersProviderMember(userId: identifier, displayName: identifier, avatarUrl: "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ")
}
}
}
extension MockCompletionSuggestionScreenState: CommandsProviderProtocol {
var isRoomAdmin: Bool { false }
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands([
CommandsProviderCommand(name: "/ban",
parametersFormat: "<user-id> [<reason>]",
description: "Bans user with given id",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/invite",
parametersFormat: "<user-id>",
description: "Invites user with given id to current room",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/join",
parametersFormat: "<room-address>",
description: "Joins room with given address",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/op",
parametersFormat: "<user-id> <power-level>",
description: "Define the power level of a user",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/deop",
parametersFormat: "<user-id>",
description: "Deops user with given id",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/me",
parametersFormat: "<message>",
description: "Displays action",
requiresAdminPowerLevel: false)
])
}
}

View file

@ -0,0 +1,85 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias CompletionSuggestionViewModelType = StateStoreViewModel<CompletionSuggestionViewState, CompletionSuggestionViewAction>
class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, CompletionSuggestionViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let completionSuggestionService: CompletionSuggestionServiceProtocol
// MARK: Public
var sharedContext: CompletionSuggestionViewModelType.Context {
context
}
var completion: ((CompletionSuggestionViewModelResult) -> Void)?
// MARK: - Setup
init(completionSuggestionService: CompletionSuggestionServiceProtocol) {
self.completionSuggestionService = completionSuggestionService
let items = completionSuggestionService.items.value.map { suggestionItem in
switch suggestionItem {
case .command(let completionSuggestionCommandItem):
return CompletionSuggestionViewStateItem.command(
name: completionSuggestionCommandItem.name,
parametersFormat: completionSuggestionCommandItem.parametersFormat,
description: completionSuggestionCommandItem.description
)
case .user(let completionSuggestionUserItem):
return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId,
avatar: completionSuggestionUserItem,
displayName: completionSuggestionUserItem.displayName)
}
}
super.init(initialViewState: CompletionSuggestionViewState(items: items))
completionSuggestionService.items.sink { [weak self] items in
self?.state.items = items.map { item in
switch item {
case .command(let completionSuggestionCommandItem):
return CompletionSuggestionViewStateItem.command(
name: completionSuggestionCommandItem.name,
parametersFormat: completionSuggestionCommandItem.parametersFormat,
description: completionSuggestionCommandItem.description
)
case .user(let completionSuggestionUserItem):
return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId,
avatar: completionSuggestionUserItem,
displayName: completionSuggestionUserItem.displayName)
}
}
}.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: CompletionSuggestionViewAction) {
switch viewAction {
case .selectedItem(let item):
completion?(.selectedItemWithIdentifier(item.id))
}
}
}

View file

@ -16,10 +16,10 @@
import Foundation import Foundation
protocol UserSuggestionViewModelProtocol { protocol CompletionSuggestionViewModelProtocol {
/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple /// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple
/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` /// `CompletionSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController`
/// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data. /// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data.
var sharedContext: UserSuggestionViewModelType.Context { get } var sharedContext: CompletionSuggestionViewModelType.Context { get }
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set }
} }

View file

@ -0,0 +1,272 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import SwiftUI
import UIKit
import WysiwygComposer
protocol CompletionSuggestionCoordinatorDelegate: AnyObject {
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?)
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?)
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
}
struct CompletionSuggestionCoordinatorParameters {
let mediaManager: MXMediaManager
let room: MXRoom
let userID: String
}
/// Wrapper around `CompletionSuggestionViewModelType.Context` to pass it through obj-c.
final class CompletionSuggestionViewModelContextWrapper: NSObject {
let context: CompletionSuggestionViewModelType.Context
init(context: CompletionSuggestionViewModelType.Context) {
self.context = context
}
}
final class CompletionSuggestionCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: CompletionSuggestionCoordinatorParameters
private var completionSuggestionHostingController: UIHostingController<AnyView>
private var completionSuggestionService: CompletionSuggestionServiceProtocol
private var completionSuggestionViewModel: CompletionSuggestionViewModelProtocol
private var roomMemberProvider: CompletionSuggestionCoordinatorRoomMemberProvider
private var commandProvider: CompletionSuggestionCoordinatorCommandProvider
private var cancellables = Set<AnyCancellable>()
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
weak var delegate: CompletionSuggestionCoordinatorDelegate?
// MARK: - Setup
init(parameters: CompletionSuggestionCoordinatorParameters) {
self.parameters = parameters
roomMemberProvider = CompletionSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
commandProvider = CompletionSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID)
completionSuggestionService = CompletionSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider)
let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
completionSuggestionViewModel = viewModel
completionSuggestionHostingController = VectorHostingController(rootView: view)
completionSuggestionViewModel.completion = { [weak self] result in
guard let self = self else {
return
}
switch result {
case .selectedItemWithIdentifier(let identifier):
if identifier == CompletionSuggestionUserID.room {
self.delegate?.completionSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.completionSuggestionService.currentTextTrigger)
return
}
if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first {
self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger)
} else if let command = self.commandProvider.commands.filter({ $0.cmd == identifier }).first {
self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.cmd, textTrigger: self.completionSuggestionService.currentTextTrigger)
}
}
}
completionSuggestionService.items.sink { [weak self] _ in
guard let self = self else { return }
self.delegate?.completionSuggestionCoordinator(self, didUpdateViewHeight: self.calculateViewHeight())
}.store(in: &cancellables)
}
func processTextMessage(_ textMessage: String) {
completionSuggestionService.processTextMessage(textMessage)
}
func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) {
completionSuggestionService.processSuggestionPattern(suggestionPattern)
}
// MARK: - Public
func start() { }
func toPresentable() -> UIViewController {
completionSuggestionHostingController
}
func sharedContext() -> CompletionSuggestionViewModelContextWrapper {
CompletionSuggestionViewModelContextWrapper(context: completionSuggestionViewModel.sharedContext)
}
// MARK: - Private
private func calculateViewHeight() -> CGFloat {
let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
let controller = VectorHostingController(rootView: view)
guard let view = controller.view else {
return 0
}
view.isHidden = true
toPresentable().view.addSubview(view)
controller.didMove(toParent: toPresentable())
view.setNeedsLayout()
view.layoutIfNeeded()
let result = view.intrinsicContentSize.height
controller.didMove(toParent: nil)
view.removeFromSuperview()
return result
}
}
private class CompletionSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
private let room: MXRoom
private let userID: String
var roomMembers: [MXRoomMember] = []
var canMentionRoom = false
init(room: MXRoom, userID: String) {
self.room = room
self.userID = userID
updateWithPowerLevels()
}
/// Gets the power levels for the room to update suggestions accordingly.
func updateWithPowerLevels() {
room.state { [weak self] state in
guard let self, let powerLevels = state?.powerLevels else { return }
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID)
let mentionRoomPowerLevel = powerLevels.minimumPowerLevel(forNotifications: kMXRoomPowerLevelNotificationsRoomKey,
defaultPower: kMXRoomPowerLevelNotificationsRoomDefault)
self.canMentionRoom = userPowerLevel >= mentionRoomPowerLevel
}
}
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
room.members { [weak self] roomMembers in
guard let self = self, let joinedMembers = roomMembers?.joinedMembers else {
return
}
self.roomMembers = joinedMembers
members(self.roomMembersToProviderMembers(joinedMembers))
} lazyLoadedMembers: { [weak self] lazyRoomMembers in
guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else {
return
}
self.roomMembers = joinedMembers
members(self.roomMembersToProviderMembers(joinedMembers))
} failure: { error in
MXLog.error("[CompletionSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error)
}
}
private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] {
roomMembers.map { RoomMembersProviderMember(userId: $0.userId, displayName: $0.displayname ?? "", avatarUrl: $0.avatarUrl ?? "") }
}
}
private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderProtocol {
private let room: MXRoom
private let userID: String
var commands = MXKSlashCommand.allCases
var isRoomAdmin = false
init(room: MXRoom, userID: String) {
self.room = room
self.userID = userID
updateWithPowerLevels()
}
func updateWithPowerLevels() {
room.state { [weak self] state in
guard let self, let powerLevels = state?.powerLevels else { return }
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID)
self.isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin
}
}
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description, requiresAdminPowerLevel: $0.requiresAdminPowerLevel) })
}
}
private extension MXKSlashCommand {
var description: String {
switch self {
case .changeDisplayName:
return VectorL10n.roomCommandChangeDisplayNameDescription
case .emote:
return VectorL10n.roomCommandEmoteDescription
case .joinRoom:
return VectorL10n.roomCommandJoinRoomDescription
case .partRoom:
return VectorL10n.roomCommandPartRoomDescription
case .inviteUser:
return VectorL10n.roomCommandInviteUserDescription
case .kickUser:
return VectorL10n.roomCommandKickUserDescription
case .banUser:
return VectorL10n.roomCommandBanUserDescription
case .unbanUser:
return VectorL10n.roomCommandUnbanUserDescription
case .setUserPowerLevel:
return VectorL10n.roomCommandSetUserPowerLevelDescription
case .resetUserPowerLevel:
return VectorL10n.roomCommandResetUserPowerLevelDescription
case .changeRoomTopic:
return VectorL10n.roomCommandChangeRoomTopicDescription
case .discardSession:
return VectorL10n.roomCommandDiscardSessionDescription
}
}
// Note: for now only filter out `/op` and `/deop` (same as Element-Web),
// but we could use power level for ban/invite/etc to filter further.
var requiresAdminPowerLevel: Bool {
switch self {
case .setUserPowerLevel, .resetUserPowerLevel:
return true
default:
return false
}
}
}

View file

@ -0,0 +1,79 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@objc
protocol CompletionSuggestionCoordinatorBridgeDelegate: AnyObject {
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinatorBridge, textTrigger: String?)
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?)
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat)
}
@objcMembers
final class CompletionSuggestionCoordinatorBridge: NSObject {
private var _completionSuggestionCoordinator: Any?
fileprivate var completionSuggestionCoordinator: CompletionSuggestionCoordinator {
_completionSuggestionCoordinator as! CompletionSuggestionCoordinator
}
weak var delegate: CompletionSuggestionCoordinatorBridgeDelegate?
init(mediaManager: MXMediaManager, room: MXRoom, userID: String) {
let parameters = CompletionSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID)
let completionSuggestionCoordinator = CompletionSuggestionCoordinator(parameters: parameters)
_completionSuggestionCoordinator = completionSuggestionCoordinator
super.init()
completionSuggestionCoordinator.delegate = self
}
func processTextMessage(_ textMessage: String) {
completionSuggestionCoordinator.processTextMessage(textMessage)
}
func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) {
completionSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern)
}
func toPresentable() -> UIViewController? {
completionSuggestionCoordinator.toPresentable()
}
func sharedContext() -> CompletionSuggestionViewModelContextWrapper {
completionSuggestionCoordinator.sharedContext()
}
}
extension CompletionSuggestionCoordinatorBridge: CompletionSuggestionCoordinatorDelegate {
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
}
func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger)
}
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger)
}
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) {
delegate?.completionSuggestionCoordinatorBridge(self, didUpdateViewHeight: height)
}
}

View file

@ -0,0 +1,231 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import WysiwygComposer
struct RoomMembersProviderMember {
var userId: String
var displayName: String
var avatarUrl: String
}
struct CommandsProviderCommand {
let name: String
let parametersFormat: String
let description: String
let requiresAdminPowerLevel: Bool
}
class CompletionSuggestionUserID: NSObject {
/// A special case added for suggesting `@room` mentions.
@objc static let room = "@room"
}
protocol RoomMembersProviderProtocol {
var canMentionRoom: Bool { get }
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
}
protocol CommandsProviderProtocol {
var isRoomAdmin: Bool { get }
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void)
}
struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol {
let userId: String
let displayName: String?
let avatarUrl: String?
}
struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol {
let name: String
let parametersFormat: String
let description: String
}
class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
// MARK: - Properties
// MARK: Private
private let roomMemberProvider: RoomMembersProviderProtocol
private let commandProvider: CommandsProviderProtocol
private var suggestionItems: [CompletionSuggestionItem] = []
private let currentTextTriggerSubject = CurrentValueSubject<TextTrigger?, Never>(nil)
private var cancellables = Set<AnyCancellable>()
// MARK: Public
var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([])
var currentTextTrigger: String? {
currentTextTriggerSubject.value?.asString()
}
// MARK: - Setup
init(roomMemberProvider: RoomMembersProviderProtocol,
commandProvider: CommandsProviderProtocol,
shouldDebounce: Bool = true) {
self.roomMemberProvider = roomMemberProvider
self.commandProvider = commandProvider
if shouldDebounce {
currentTextTriggerSubject
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) }
.store(in: &cancellables)
} else {
currentTextTriggerSubject
.sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) }
.store(in: &cancellables)
}
}
// MARK: - CompletionSuggestionServiceProtocol
func processTextMessage(_ textMessage: String?) {
guard let textMessage = textMessage,
let textTrigger = textMessage.currentTextTrigger
else {
items.send([])
currentTextTriggerSubject.send(nil)
return
}
currentTextTriggerSubject.send(textTrigger)
}
func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) {
guard let suggestionPattern else {
items.send([])
currentTextTriggerSubject.send(nil)
return
}
switch suggestionPattern.key {
case .at:
currentTextTriggerSubject.send(TextTrigger(key: .at, text: suggestionPattern.text))
case .hash:
// No room suggestion support yet
items.send([])
currentTextTriggerSubject.send(nil)
case .slash:
currentTextTriggerSubject.send(TextTrigger(key: .slash, text: suggestionPattern.text))
}
}
// MARK: - Private
private func fetchAndFilterSuggestionsForTextTrigger(_ textTrigger: TextTrigger?) {
guard let textTrigger else { return }
switch textTrigger.key {
case .at:
roomMemberProvider.fetchMembers { [weak self] members in
guard let self = self else {
return
}
self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in
CompletionSuggestionItem.user(value: CompletionSuggestionServiceUserItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl))
}
self.items.send(self.suggestionItems.filter { item in
guard case let .user(completionSuggestionUserItem) = item else { return false }
let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(textTrigger.text.lowercased())
let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(textTrigger.text.lowercased())
return (containedInUsername || containedInDisplayName)
})
}
case .slash:
commandProvider.fetchCommands { [weak self] commands in
guard let self else { return }
self.suggestionItems = commands.filtered(isRoomAdmin: self.commandProvider.isRoomAdmin).map { command in
CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(
name: command.name,
parametersFormat: command.parametersFormat,
description: command.description
))
}
if textTrigger.text.isEmpty {
// A single `/` will display all available commands.
self.items.send(self.suggestionItems)
} else {
self.items.send(self.suggestionItems.filter { item in
guard case let .command(commandSuggestion) = item else { return false }
return commandSuggestion.name.lowercased().contains(textTrigger.text.lowercased())
})
}
}
}
}
}
extension Array where Element == RoomMembersProviderMember {
/// Returns the array with an additional member that represents an `@room` mention.
func withRoom(_ canMentionRoom: Bool) -> Self {
guard canMentionRoom else { return self }
return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")]
}
}
extension Array where Element == CommandsProviderCommand {
func filtered(isRoomAdmin: Bool) -> Self {
guard !isRoomAdmin else { return self }
return filter { !$0.requiresAdminPowerLevel }
}
}
private enum SuggestionKey: Character {
case at = "@"
case slash = "/"
}
private struct TextTrigger: Equatable {
let key: SuggestionKey
let text: String
func asString() -> String {
String(key.rawValue) + text
}
}
private extension String {
// Returns current completion suggestion for a text message, if any.
var currentTextTrigger: TextTrigger? {
let components = components(separatedBy: .whitespaces)
guard var lastComponent = components.last,
lastComponent.count > 0,
let suggestionKey = SuggestionKey(rawValue: lastComponent.removeFirst()),
// If a second character exists and is the same as the key it shouldn't trigger.
lastComponent.first != suggestionKey.rawValue,
// Slash commands should be displayed only if there is a single component
!(suggestionKey == .slash && components.count > 1)
else { return nil }
return TextTrigger(key: suggestionKey, text: lastComponent)
}
}

View file

@ -18,14 +18,25 @@ import Combine
import Foundation import Foundation
import WysiwygComposer import WysiwygComposer
protocol UserSuggestionItemProtocol: Avatarable { protocol CompletionSuggestionUserItemProtocol: Avatarable {
var userId: String { get } var userId: String { get }
var displayName: String? { get } var displayName: String? { get }
var avatarUrl: String? { get } var avatarUrl: String? { get }
} }
protocol UserSuggestionServiceProtocol { protocol CompletionSuggestionCommandItemProtocol {
var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } var name: String { get }
var parametersFormat: String { get }
var description: String { get }
}
enum CompletionSuggestionItem {
case command(value: CompletionSuggestionCommandItemProtocol)
case user(value: CompletionSuggestionUserItemProtocol)
}
protocol CompletionSuggestionServiceProtocol {
var items: CurrentValueSubject<[CompletionSuggestionItem], Never> { get }
var currentTextTrigger: String? { get } var currentTextTrigger: String? { get }
@ -35,7 +46,7 @@ protocol UserSuggestionServiceProtocol {
// MARK: Avatarable // MARK: Avatarable
extension UserSuggestionItemProtocol { extension CompletionSuggestionUserItemProtocol {
var mxContentUri: String? { var mxContentUri: String? {
avatarUrl avatarUrl
} }

View file

@ -17,9 +17,9 @@
import RiotSwiftUI import RiotSwiftUI
import XCTest import XCTest
class UserSuggestionUITests: MockScreenTestCase { class CompletionSuggestionUITests: MockScreenTestCase {
func testUserSuggestionScreen() throws { func testCompletionSuggestionScreen() throws {
app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title) app.goToScreenWithIdentifier(MockCompletionSuggestionScreenState.multipleResults.title)
let firstButton = app.buttons["displayNameText-userIdText"].firstMatch let firstButton = app.buttons["displayNameText-userIdText"].firstMatch
XCTAssert(firstButton.waitForExistence(timeout: 10)) XCTAssert(firstButton.waitForExistence(timeout: 10))

Some files were not shown because too many files have changed in this diff Show more