mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #7549 from vector-im/release/1.10.12/release
Release 1.10.12
This commit is contained in:
commit
3c478e0581
133 changed files with 2633 additions and 2388 deletions
248
.github/workflows/triage-move-labelled.yml
vendored
248
.github/workflows/triage-move-labelled.yml
vendored
|
@ -53,23 +53,10 @@ jobs:
|
|||
contains(github.event.issue.labels.*.name, 'O-Frequent')) ||
|
||||
contains(github.event.issue.labels.*.name, 'A11y'))
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc0sUA"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/18
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
add_product_issues_to_project:
|
||||
name: X-Needs-Product to Design project board
|
||||
|
@ -77,138 +64,10 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_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 }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/28
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ex_plorers:
|
||||
name: Add labelled issues to X-Plorer project
|
||||
|
@ -216,23 +75,10 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Element X Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc4ALoFY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/73
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ps_features1:
|
||||
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-User-Settings'))
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc4AHJKF"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/56
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ps_features2:
|
||||
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-Broadcast')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc4AHJKd"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/58
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
ps_features3:
|
||||
name: Add labelled issues to PS features team 3
|
||||
|
@ -294,23 +114,10 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc4AHJKW"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/57
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
voip:
|
||||
name: Add labelled issues to VoIP project board
|
||||
|
@ -318,20 +125,7 @@ jobs:
|
|||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: VoIP')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
- uses: actions/add-to-project@main
|
||||
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_kwDOAM0swc4ABMIk"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: https://github.com/orgs/vector-im/projects/41
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
31
CHANGES.md
31
CHANGES.md
|
@ -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)
|
||||
|
||||
🙌 Improvements
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.10.11
|
||||
CURRENT_PROJECT_VERSION = 1.10.11
|
||||
MARKETING_VERSION = 1.10.12
|
||||
CURRENT_PROJECT_VERSION = 1.10.12
|
||||
|
|
|
@ -98,8 +98,13 @@ final class BuildSettings: NSObject {
|
|||
|
||||
// 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"
|
||||
|
||||
/// Default identity server
|
||||
static let serverConfigDefaultIdentityServerUrlString = "https://vector.im"
|
||||
|
||||
static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify"
|
||||
|
|
|
@ -92,8 +92,7 @@ class CommonConfiguration: NSObject, Configurable {
|
|||
|
||||
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
|
||||
|
||||
// Configure Crypto SDK feature deciding which crypto module to use
|
||||
sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared
|
||||
sdkOptions.cryptoMigrationDelegate = self
|
||||
}
|
||||
|
||||
private func makeASCIIUserAgent() -> String? {
|
||||
|
@ -169,13 +168,15 @@ class CommonConfiguration: NSObject, Configurable {
|
|||
callManager.fallbackSTUNServer = stunServerFallback
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Per loaded matrix session settings
|
||||
|
||||
func setupSettingsWhenLoaded(for matrixSession: MXSession) {
|
||||
// Do not warn for unknown devices. We have cross-signing now
|
||||
(matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CommonConfiguration: MXCryptoV2MigrationDelegate {
|
||||
var needsVerificationUpgrade: Bool {
|
||||
get {
|
||||
RiotSettings.shared.showVerificationUpgradeAlert
|
||||
}
|
||||
set {
|
||||
RiotSettings.shared.showVerificationUpgradeAlert = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,4 @@ import MatrixSDK
|
|||
|
||||
// MARK: - Per matrix session settings
|
||||
func setupSettings(for matrixSession: MXSession)
|
||||
|
||||
// MARK: - Per loaded matrix session settings
|
||||
func setupSettingsWhenLoaded(for matrixSession: MXSession)
|
||||
}
|
||||
|
|
2
Podfile
2
Podfile
|
@ -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
|
||||
#
|
||||
# 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 = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
|
|
@ -50,8 +50,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
|
||||
"state" : {
|
||||
"revision" : "758f226a92d6726ab626c1e78ecd183bdba77016",
|
||||
"version" : "2.0.0"
|
||||
"revision" : "ff5e8054da60212051cb0dec244500ca0f441bac",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
BIN
Riot/Assets/Sounds/vberror.mp3
Normal file
BIN
Riot/Assets/Sounds/vberror.mp3
Normal file
Binary file not shown.
|
@ -614,6 +614,21 @@ Tap the + to start adding people.";
|
|||
"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 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
|
||||
"room_thread_title" = "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_wysiwyg_composer" = "Try out the rich text editor";
|
||||
"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_olm_version" = "Olm Version %@";
|
||||
|
@ -2393,6 +2405,8 @@ Tap the + to start adding people.";
|
|||
|
||||
"poll_timeline_reply_ended_poll" = "Ended poll";
|
||||
|
||||
"poll_timeline_loading" = "Loading...";
|
||||
|
||||
// MARK: - Location sharing
|
||||
|
||||
"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_display_name_set" = "%@ set their display name 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_topic_changed" = "%@ changed the topic to \"%@\".";
|
||||
"notice_room_name_changed" = "%@ changed the room name to %@.";
|
||||
|
|
|
@ -70,7 +70,6 @@ extension MXBugReportRestClient {
|
|||
|
||||
// SDKs
|
||||
userInfo["matrix_sdk_version"] = MatrixSDKVersion
|
||||
userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId
|
||||
if let crypto = mainAccount?.mxSession?.crypto {
|
||||
userInfo["crypto_module_version"] = crypto.version
|
||||
}
|
||||
|
|
|
@ -256,40 +256,18 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
|
|||
|
||||
if (componentIndex < bubbleComponents.count)
|
||||
{
|
||||
MXKRoomBubbleComponent *component = bubbleComponents[componentIndex];
|
||||
|
||||
// Define the marker frame
|
||||
CGFloat markPosY = component.position.y + self.msgTextViewTopConstraint.constant;
|
||||
|
||||
NSInteger mostRecentComponentIndex = bubbleComponents.count - 1;
|
||||
if ([bubbleData isKindOfClass:RoomBubbleCellData.class])
|
||||
CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex];
|
||||
if (CGRectIsEmpty(componentFrame))
|
||||
{
|
||||
mostRecentComponentIndex = ((RoomBubbleCellData*)bubbleData).mostRecentComponentIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the mark height.
|
||||
// Use the rest of the cell height by default.
|
||||
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];
|
||||
CGRect markerFrame = CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X,
|
||||
CGRectGetMinY(componentFrame),
|
||||
VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH,
|
||||
CGRectGetHeight(componentFrame));
|
||||
|
||||
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,
|
||||
markHeight)];
|
||||
UIView *markerView = [[UIView alloc] initWithFrame:markerFrame];
|
||||
markerView.backgroundColor = ThemeService.shared.theme.tintColor;
|
||||
|
||||
[markerView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
@ -303,28 +281,28 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
|
|||
toItem:self.contentView
|
||||
attribute:NSLayoutAttributeLeading
|
||||
multiplier:1.0
|
||||
constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X];
|
||||
constant:CGRectGetMinX(markerFrame)];
|
||||
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.contentView
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:markPosY];
|
||||
constant:CGRectGetMinY(markerFrame)];
|
||||
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView
|
||||
attribute:NSLayoutAttributeWidth
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH];
|
||||
constant:CGRectGetWidth(markerFrame)];
|
||||
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView
|
||||
attribute:NSLayoutAttributeHeight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:nil
|
||||
attribute:NSLayoutAttributeNotAnAttribute
|
||||
multiplier:1.0
|
||||
constant:markHeight];
|
||||
constant:CGRectGetHeight(markerFrame)];
|
||||
|
||||
// Available on iOS 8 and later
|
||||
[NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]];
|
||||
|
@ -600,19 +578,29 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
|
|||
}
|
||||
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]])
|
||||
{
|
||||
RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData;
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -620,14 +608,15 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
|
|||
}
|
||||
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
|
||||
[roomBubbleTableViewCell setNeedsLayout];
|
||||
[roomBubbleTableViewCell layoutIfNeeded];
|
||||
// Get the vertical position of the messageTextView relative to the contentView
|
||||
selectedComponenContentViewYOffset = CGRectGetMinY(messageTextViewFrame);
|
||||
|
||||
selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y;
|
||||
// Get the position of the component inside the messageTextView
|
||||
selectedComponentPositionY = selectedComponent.position.y;
|
||||
}
|
||||
|
||||
if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView)
|
||||
|
@ -801,8 +790,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed =
|
|||
|
||||
- (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);
|
||||
|
||||
[self.contentView addSubview:tickView];
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
#import "AvatarGenerator.h"
|
||||
#import "MatrixKit.h"
|
||||
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation MXRoom (Riot)
|
||||
|
@ -331,30 +331,10 @@
|
|||
{
|
||||
[self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) {
|
||||
|
||||
UserEncryptionTrustLevel userEncryptionTrustLevel;
|
||||
double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted;
|
||||
|
||||
if (trustedDevicesPercentage >= 1.0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
MXCrossSigningInfo *crossSigningInfo = [self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId];
|
||||
EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
|
||||
UserEncryptionTrustLevel userEncryptionTrustLevel = [encryption userTrustLevelWithCrossSigning:crossSigningInfo
|
||||
trustedDevicesProgress:usersTrustLevelSummary.trustedDevicesProgress];
|
||||
onComplete(userEncryptionTrustLevel);
|
||||
|
||||
} failure:^(NSError *error) {
|
||||
|
|
|
@ -15,17 +15,7 @@
|
|||
*/
|
||||
|
||||
#import "MatrixKit.h"
|
||||
|
||||
/**
|
||||
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
|
||||
RoomEncryptionTrustLevelTrusted,
|
||||
RoomEncryptionTrustLevelWarning,
|
||||
RoomEncryptionTrustLevelNormal,
|
||||
RoomEncryptionTrustLevelUnknown
|
||||
};
|
||||
|
||||
#import "RoomEncryptionTrustLevel.h"
|
||||
|
||||
/**
|
||||
Define a `MXRoomSummary` category at Riot level.
|
||||
|
|
|
@ -33,32 +33,15 @@
|
|||
|
||||
- (RoomEncryptionTrustLevel)roomEncryptionTrustLevel
|
||||
{
|
||||
RoomEncryptionTrustLevel roomEncryptionTrustLevel = RoomEncryptionTrustLevelUnknown;
|
||||
if (self.trust)
|
||||
MXUsersTrustLevelSummary *trust = self.trust;
|
||||
if (!trust)
|
||||
{
|
||||
double trustedUsersPercentage = self.trust.trustedUsersProgress.fractionCompleted;
|
||||
double trustedDevicesPercentage = self.trust.trustedDevicesProgress.fractionCompleted;
|
||||
|
||||
if (trustedUsersPercentage >= 1.0)
|
||||
{
|
||||
if (trustedDevicesPercentage >= 1.0)
|
||||
{
|
||||
roomEncryptionTrustLevel = RoomEncryptionTrustLevelTrusted;
|
||||
}
|
||||
else
|
||||
{
|
||||
roomEncryptionTrustLevel = RoomEncryptionTrustLevelWarning;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
roomEncryptionTrustLevel = RoomEncryptionTrustLevelNormal;
|
||||
}
|
||||
|
||||
roomEncryptionTrustLevel = roomEncryptionTrustLevel;
|
||||
MXLogError(@"[MXRoomSummary] roomEncryptionTrustLevel: trust is missing");
|
||||
return RoomEncryptionTrustLevelUnknown;
|
||||
}
|
||||
|
||||
return roomEncryptionTrustLevel;
|
||||
EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init];
|
||||
return [encryption roomTrustLevelWithSummary:trust];
|
||||
}
|
||||
|
||||
- (BOOL)isJoined
|
||||
|
|
64
Riot/Categories/NSAttributedString+Theme.swift
Normal file
64
Riot/Categories/NSAttributedString+Theme.swift
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3899,6 +3899,10 @@ public class VectorL10n: NSObject {
|
|||
public static func noticeDisplayNameChangedFromByYou(_ p1: String, _ p2: String) -> String {
|
||||
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
|
||||
public static func noticeDisplayNameRemoved(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "notice_display_name_removed", p1)
|
||||
|
@ -4923,6 +4927,10 @@ public class VectorL10n: NSObject {
|
|||
public static var pollTimelineEndedText: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_ended_text")
|
||||
}
|
||||
/// Loading...
|
||||
public static var pollTimelineLoading: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_loading")
|
||||
}
|
||||
/// Please try again
|
||||
public static var pollTimelineNotClosedSubtitle: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle")
|
||||
|
@ -5211,6 +5219,58 @@ public class VectorL10n: NSObject {
|
|||
public static var roomAvatarViewAccessibilityLabel: String {
|
||||
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
|
||||
public static var roomConferenceCallNoPower: String {
|
||||
return VectorL10n.tr("Vector", "room_conference_call_no_power")
|
||||
|
@ -7647,18 +7707,10 @@ public class VectorL10n: NSObject {
|
|||
public static var settingsLabs: String {
|
||||
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
|
||||
public static var settingsLabsCreateConferenceWithJitsi: String {
|
||||
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
|
||||
public static var settingsLabsE2eEncryption: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption")
|
||||
|
@ -7671,10 +7723,6 @@ public class VectorL10n: NSObject {
|
|||
public static var settingsLabsEnableAutoReportDecryptionErrors: String {
|
||||
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)
|
||||
public static var settingsLabsEnableLiveLocationSharing: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing")
|
||||
|
|
|
@ -274,7 +274,7 @@ extension Analytics {
|
|||
func trackE2EEError(_ reason: DecryptionFailureReason, context: String) {
|
||||
let event = AnalyticsEvent.Error(
|
||||
context: context,
|
||||
cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native,
|
||||
cryptoModule: .Rust,
|
||||
domain: .E2EE,
|
||||
name: reason.errorName
|
||||
)
|
||||
|
|
|
@ -46,9 +46,6 @@ struct SentryMonitoringClient {
|
|||
if let message = event.message?.formatted {
|
||||
event.fingerprint = [message]
|
||||
}
|
||||
event.tags = [
|
||||
"crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId
|
||||
]
|
||||
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
|
||||
return event
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
#import "ContactDetailsViewController.h"
|
||||
|
||||
#import "BugReportViewController.h"
|
||||
#import "RoomKeyRequestViewController.h"
|
||||
#import "DecryptionFailureTracker.h"
|
||||
|
||||
#import "Tools.h"
|
||||
|
@ -114,11 +113,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
id roomKeyRequestObserver;
|
||||
id roomKeyRequestCancellationObserver;
|
||||
|
||||
/**
|
||||
If any the currently displayed sharing key dialog
|
||||
*/
|
||||
RoomKeyRequestViewController *roomKeyRequestViewController;
|
||||
|
||||
/**
|
||||
Incoming key verification requests observers
|
||||
*/
|
||||
|
@ -396,6 +390,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
}
|
||||
[NSBundle mxk_setLanguage:language];
|
||||
[NSBundle mxk_setFallbackLanguage:@"en"];
|
||||
UIApplication.sharedApplication.accessibilityLanguage = language;
|
||||
|
||||
if (BuildSettings.disableRightToLeftLayout)
|
||||
{
|
||||
|
@ -1823,8 +1818,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
// start the call service
|
||||
[self.callPresenter start];
|
||||
|
||||
[self.configuration setupSettingsWhenLoadedFor:mxSession];
|
||||
|
||||
// Register to user new device sign in notification
|
||||
[self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession];
|
||||
|
||||
|
@ -1833,8 +1826,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
// Register to new key verification request
|
||||
[self registerNewRequestNotificationForSession:mxSession];
|
||||
|
||||
[self checkLocalPrivateKeysInSession:mxSession];
|
||||
|
||||
[self.pushNotificationService checkPushKitPushersInSession:mxSession];
|
||||
}
|
||||
else if (mxSession.state == MXSessionStateRunning)
|
||||
|
@ -2031,9 +2022,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
// If any, disable the no VoIP support workaround
|
||||
[self disableNoVoIPOnMatrixSession:mxSession];
|
||||
|
||||
// Disable listening of incoming key share requests
|
||||
[self disableRoomKeyRequestObserver:mxSession];
|
||||
|
||||
// Disable listening of incoming key verification requests
|
||||
[self disableIncomingKeyVerificationObserver:mxSession];
|
||||
|
||||
|
@ -2183,9 +2171,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
// Clear cache
|
||||
[self clearCache];
|
||||
|
||||
// Reset Crypto SDK configuration (labs flag for which crypto module to use)
|
||||
[CryptoSDKFeature.shared reset];
|
||||
|
||||
// Reset key backup banner preferences
|
||||
[SecureBackupBannerPreferences.shared reset];
|
||||
|
||||
|
@ -2296,11 +2281,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
case MXSessionStateSyncInProgress:
|
||||
// Stay in launching during the first server sync if the store is empty.
|
||||
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;
|
||||
case MXSessionStateRunning:
|
||||
self.clearingCache = NO;
|
||||
|
@ -2360,7 +2340,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
|
||||
// This is the time to check existing requests
|
||||
MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests");
|
||||
[self checkPendingRoomKeyRequests];
|
||||
[self checkPendingIncomingKeyVerificationsInSession:mainSession];
|
||||
|
||||
// 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");
|
||||
|
||||
// Enable listening of incoming key share requests
|
||||
[self enableRoomKeyRequestObserver:mainSession];
|
||||
|
||||
// Enable listening of incoming key verification requests
|
||||
[self enableIncomingKeyVerificationObserver:mainSession];
|
||||
}
|
||||
|
@ -2397,16 +2373,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
{
|
||||
MXLogDebug(@"[AppDelegate] showLaunchAnimation");
|
||||
|
||||
LaunchLoadingView *launchLoadingView;
|
||||
if (MXSDKOptions.sharedInstance.enableStartupProgress)
|
||||
{
|
||||
MXSession *mainSession = self.mxSessions.firstObject;
|
||||
launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress];
|
||||
}
|
||||
else
|
||||
{
|
||||
launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil];
|
||||
}
|
||||
MXSession *mainSession = self.mxSessions.firstObject;
|
||||
LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress];
|
||||
|
||||
launchLoadingView.frame = window.bounds;
|
||||
[launchLoadingView updateWithTheme:ThemeService.shared.theme];
|
||||
|
@ -2520,38 +2488,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
#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
|
||||
{
|
||||
[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
|
||||
|
||||
- (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession
|
||||
|
@ -3785,12 +3554,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
|
||||
- (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];
|
||||
}
|
||||
|
||||
|
|
|
@ -130,8 +130,15 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
}
|
||||
|
||||
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 {
|
||||
// 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)
|
||||
} catch {
|
||||
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.
|
||||
private func showLoadingAnimation() {
|
||||
let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil
|
||||
let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
|
||||
let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress)
|
||||
loadingViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
// Replace the navigation stack with the loading animation
|
||||
|
@ -759,12 +765,6 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate {
|
|||
// MARK: - KeyVerificationCoordinatorDelegate
|
||||
extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
|
||||
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
|
||||
self?.authenticationDidComplete()
|
||||
}
|
||||
|
|
|
@ -132,7 +132,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
|
|||
target:self
|
||||
action:@selector(onButtonPressed:)];
|
||||
|
||||
self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString;
|
||||
if (BuildSettings.forceHomeserverSelection)
|
||||
{
|
||||
self.defaultHomeServerUrl = nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString;
|
||||
}
|
||||
|
||||
self.defaultIdentityServerUrl = RiotSettings.shared.identityServerUrlString;
|
||||
|
||||
|
@ -1207,7 +1214,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
|
|||
[self saveCustomServerInputs];
|
||||
|
||||
// Restore default configuration
|
||||
[self setHomeServerTextFieldText:self.defaultHomeServerUrl];
|
||||
if (BuildSettings.forceHomeserverSelection)
|
||||
{
|
||||
[self setHomeServerTextFieldText:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self setHomeServerTextFieldText:self.defaultHomeServerUrl];
|
||||
}
|
||||
[self setIdentityServerTextFieldText:self.defaultIdentityServerUrl];
|
||||
|
||||
[self.customServersTickButton setImage:AssetImages.selectionUntick.image forState:UIControlStateNormal];
|
||||
|
|
|
@ -106,8 +106,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
// MARK: - Private
|
||||
|
||||
private func showLoadingAnimation() {
|
||||
let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil
|
||||
let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
|
||||
let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress)
|
||||
loadingViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
// Replace the navigation stack with the loading animation
|
||||
|
@ -220,12 +219,6 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate
|
|||
// MARK: - KeyVerificationCoordinatorDelegate
|
||||
extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
|
||||
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
|
||||
self?.authenticationDidComplete()
|
||||
}
|
||||
|
|
|
@ -68,14 +68,7 @@ class SessionVerificationListener {
|
|||
return
|
||||
}
|
||||
|
||||
if session.state == .storeDataReady {
|
||||
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 {
|
||||
if session.state == .running {
|
||||
unregisterSessionStateChangeNotification()
|
||||
|
||||
if let crypto = session.crypto {
|
||||
|
@ -101,7 +94,6 @@ class SessionVerificationListener {
|
|||
self.completion?(.authenticationIsComplete)
|
||||
} failure: { error in
|
||||
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error)
|
||||
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
|
||||
self.completion?(.authenticationIsComplete)
|
||||
}
|
||||
} else {
|
||||
|
@ -111,12 +103,10 @@ class SessionVerificationListener {
|
|||
self.completion?(.authenticationIsComplete)
|
||||
} failure: { error in
|
||||
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.")
|
||||
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
|
||||
self.completion?(.authenticationIsComplete)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
|
||||
self.completion?(.authenticationIsComplete)
|
||||
}
|
||||
case .crossSigningExists:
|
||||
|
@ -124,13 +114,10 @@ class SessionVerificationListener {
|
|||
self.completion?(.needsVerification)
|
||||
default:
|
||||
MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do")
|
||||
|
||||
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
|
||||
self.completion?(.authenticationIsComplete)
|
||||
}
|
||||
} failure: { [weak self] error in
|
||||
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error)
|
||||
(crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
|
||||
self?.completion?(.authenticationIsComplete)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -371,27 +371,15 @@ CallAudioRouteMenuViewDelegate>
|
|||
typeof(self) self = weakSelf;
|
||||
self->currentAlert = nil;
|
||||
|
||||
// Acknowledge the existence of all devices
|
||||
[self startActivityIndicator];
|
||||
if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
|
||||
// Retry the call
|
||||
if (call.isIncoming)
|
||||
{
|
||||
MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices");
|
||||
return;
|
||||
[call answer];
|
||||
}
|
||||
else
|
||||
{
|
||||
[call callWithVideo:call.isVideoCall];
|
||||
}
|
||||
[(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{
|
||||
|
||||
[self stopActivityIndicator];
|
||||
|
||||
// Retry the call
|
||||
if (call.isIncoming)
|
||||
{
|
||||
[call answer];
|
||||
}
|
||||
else
|
||||
{
|
||||
[call callWithVideo:call.isVideoCall];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
}]];
|
||||
|
|
|
@ -103,12 +103,17 @@ class AvatarView: UIView, Themable {
|
|||
|
||||
func updateAvatarImageView(with viewData: AvatarViewDataProtocol) {
|
||||
guard let avatarImageView = self.avatarImageView else {
|
||||
MXLog.warning("[AvatarView] avatar not updated because avatarImageView is nil.")
|
||||
return
|
||||
}
|
||||
|
||||
let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill)
|
||||
updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
|
||||
|
||||
if defaultAvatarImage == nil {
|
||||
MXLog.warning("[AvatarView] defaultAvatarImage is nil")
|
||||
}
|
||||
|
||||
if let avatarUrl = viewData.avatarUrl {
|
||||
avatarImageView.setImageURI(avatarUrl,
|
||||
withType: nil,
|
||||
|
@ -118,6 +123,10 @@ class AvatarView: UIView, Themable {
|
|||
previewImage: defaultAvatarImage,
|
||||
mediaManager: viewData.mediaManager)
|
||||
updateAvatarContentMode(contentMode: .scaleAspectFill)
|
||||
|
||||
if avatarImageView.frame.size.width < 8 || avatarImageView.frame.size.height < 8 {
|
||||
MXLog.warning("[AvatarView] small avatarImageView frame: \(avatarImageView.frame)")
|
||||
}
|
||||
} else {
|
||||
updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
|
||||
}
|
||||
|
|
|
@ -81,7 +81,8 @@
|
|||
// Manage lastEventAttributedTextMessage optional property
|
||||
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
|
||||
{
|
||||
|
|
68
Riot/Modules/Encryption/EncryptionTrustLevel.swift
Normal file
68
Riot/Modules/Encryption/EncryptionTrustLevel.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
// 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.
|
||||
|
@ -14,22 +14,12 @@
|
|||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum UserSuggestionViewAction {
|
||||
case selectedItem(UserSuggestionViewStateItem)
|
||||
}
|
||||
|
||||
enum UserSuggestionViewModelResult {
|
||||
case selectedItemWithIdentifier(String)
|
||||
}
|
||||
|
||||
struct UserSuggestionViewStateItem: Identifiable {
|
||||
let id: String
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
struct UserSuggestionViewState: BindableState {
|
||||
var items: [UserSuggestionViewStateItem]
|
||||
}
|
||||
/**
|
||||
RoomEncryptionTrustLevel represents the trust level in an encrypted room.
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) {
|
||||
RoomEncryptionTrustLevelTrusted,
|
||||
RoomEncryptionTrustLevelWarning,
|
||||
RoomEncryptionTrustLevelNormal,
|
||||
RoomEncryptionTrustLevelUnknown
|
||||
};
|
|
@ -387,7 +387,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
|||
}
|
||||
|
||||
private func updateAvatarButtonItem() {
|
||||
MXLog.info("[AllChatsCoordinator] updating avatar button item.")
|
||||
if let avatar = userAvatarViewData(from: currentMatrixSession) {
|
||||
if avatarMenuView == nil {
|
||||
MXLog.warning("[AllChatsCoordinator] updateAvatarButtonItem: avatarMenuView is nil.")
|
||||
}
|
||||
avatarMenuView?.fill(with: avatar)
|
||||
avatarMenuButton?.setImage(nil, for: .normal)
|
||||
} else {
|
||||
|
|
|
@ -988,8 +988,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol {
|
|||
let title: String
|
||||
let message: String
|
||||
|
||||
if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature,
|
||||
feature.isEnabled && feature.needsVerificationUpgrade {
|
||||
if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true {
|
||||
title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle
|
||||
message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage
|
||||
} else {
|
||||
|
|
|
@ -24,4 +24,9 @@ class VerifyEmojiCollectionViewCell: UICollectionViewCell, Reusable, Themable {
|
|||
func update(theme: Theme) {
|
||||
name.textColor = theme.textPrimaryColor
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
emoji.isAccessibilityElement = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,9 +69,6 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
|
|||
|
||||
extension LaunchLoadingView: MXSessionStartupProgressDelegate {
|
||||
func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) {
|
||||
guard MXSDKOptions.sharedInstance().enableStartupProgress else {
|
||||
return
|
||||
}
|
||||
update(with: state)
|
||||
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -17,3 +17,4 @@
|
|||
#import "MXKRoomBubbleCellData.h"
|
||||
#import "UserIndicatorCancel.h"
|
||||
#import "VoiceBroadcastInfo.h"
|
||||
#import "MXKSoundPlayer.h"
|
||||
|
|
|
@ -145,5 +145,3 @@
|
|||
|
||||
#import "MXKCountryPickerViewController.h"
|
||||
#import "MXKLanguagePickerViewController.h"
|
||||
|
||||
#import "MXKSlashCommands.h"
|
||||
|
|
|
@ -947,14 +947,6 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
|
|||
|
||||
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
|
||||
[mxSession.scanManager deleteAllAntivirusScans];
|
||||
[mxSession.aggregations resetData];
|
||||
|
|
|
@ -144,6 +144,15 @@
|
|||
*/
|
||||
- (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.
|
||||
CAUTION: This method runs only on main thread.
|
||||
|
|
|
@ -500,23 +500,34 @@
|
|||
|
||||
// Return the raw height of the provided text by removing any margin
|
||||
- (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;
|
||||
if ([NSThread currentThread] != [NSThread mainThread])
|
||||
{
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
textSize = [self textContentSize:attributedText removeVerticalInset:YES];
|
||||
textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
textSize = [self textContentSize:attributedText removeVerticalInset:YES];
|
||||
textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
|
||||
}
|
||||
|
||||
return textSize.height;
|
||||
}
|
||||
|
||||
- (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* measurementTextViewWithoutInset = nil;
|
||||
|
@ -536,7 +547,7 @@
|
|||
// Select the right text view for measurement
|
||||
UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView);
|
||||
|
||||
selectedTextView.frame = CGRectMake(0, 0, _maxTextViewWidth, 0);
|
||||
selectedTextView.frame = CGRectMake(0, 0, maxTextViewWidth, 0);
|
||||
selectedTextView.attributedText = attributedText;
|
||||
|
||||
// Force the layout manager to layout the text, fixes problems starting iOS 16
|
||||
|
|
|
@ -31,8 +31,6 @@
|
|||
|
||||
#import "MXKAppSettings.h"
|
||||
|
||||
#import "MXKSlashCommands.h"
|
||||
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
const BOOL USE_THREAD_TIMELINE = YES;
|
||||
|
@ -316,7 +314,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
|||
|
||||
_filterMessagesWithURL = NO;
|
||||
|
||||
emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", kMXKSlashCmdEmote];
|
||||
emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]];
|
||||
|
||||
// Set default data and view classes
|
||||
// Cell data
|
||||
|
@ -458,11 +456,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
|||
}
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
[self resetNotifying:YES];
|
||||
}
|
||||
|
||||
- (void)resetNotifying:(BOOL)notify
|
||||
{
|
||||
if (roomDidFlushDataNotificationObserver)
|
||||
{
|
||||
|
@ -558,12 +551,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
|||
}
|
||||
|
||||
_serverSyncEventCount = 0;
|
||||
|
||||
// Notify the delegate to reload its tableview
|
||||
if (notify && self.delegate)
|
||||
{
|
||||
[self.delegate dataSource:self didCellChange:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reload
|
||||
|
@ -577,10 +564,16 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
|||
|
||||
[self setState:MXKDataSourceStatePreparing];
|
||||
|
||||
[self resetNotifying:notify];
|
||||
[self reset];
|
||||
|
||||
// Reload
|
||||
[self didMXSessionStateChange];
|
||||
|
||||
// Notify the delegate to refresh the tableview
|
||||
if (notify && self.delegate)
|
||||
{
|
||||
[self.delegate dataSource:self didCellChange:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
|
|
|
@ -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;
|
|
@ -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";
|
101
Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift
Normal file
101
Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift
Normal 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 ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -571,7 +571,7 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
|
|||
}
|
||||
else
|
||||
{
|
||||
displayText = [VectorL10n noticeDisplayNameChangedFrom:event.sender :prevDisplayname :displayname];
|
||||
displayText = [VectorL10n noticeDisplayNameChangedTo:prevDisplayname :displayname];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,14 @@ typedef enum : NSUInteger
|
|||
*/
|
||||
- (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.
|
||||
|
||||
|
@ -205,6 +213,15 @@ typedef enum : NSUInteger
|
|||
*/
|
||||
- (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
|
||||
|
||||
/**
|
||||
|
@ -382,6 +399,11 @@ typedef enum : NSUInteger
|
|||
*/
|
||||
@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.
|
||||
*/
|
||||
|
|
|
@ -1405,4 +1405,9 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes)
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (void)setPartialContent:(NSAttributedString *)attributedTextMessage
|
||||
{
|
||||
self.attributedTextMessage = attributedTextMessage;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -19,7 +19,7 @@ import Foundation
|
|||
extension RoomDataSource {
|
||||
// MARK: - Private Constants
|
||||
private enum Constants {
|
||||
static let emoteMessageSlashCommandPrefix = String(format: "%@ ", kMXKSlashCmdEmote)
|
||||
static let emoteMessageSlashCommandPrefix = String(format: "%@ ", MXKSlashCommand.emote.cmd)
|
||||
}
|
||||
|
||||
// MARK: - NSAttributedString Sending
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
#import "MXKEncryptionKeysImportView.h"
|
||||
|
||||
#import "NSBundle+MatrixKit.h"
|
||||
#import "MXKSlashCommands.h"
|
||||
#import "MXKSwiftHeader.h"
|
||||
|
||||
#import "MXKPreviewViewController.h"
|
||||
|
@ -361,7 +360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
{
|
||||
// Retrieve the potential message partially typed during last room display.
|
||||
// Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
|
||||
inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage;
|
||||
[inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
[self sendTextMessage:string];
|
||||
|
@ -1320,7 +1325,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// Display cmd usage in text input as placeholder
|
||||
cmdUsage = @"Usage: /nick <display_name>";
|
||||
cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName];
|
||||
}
|
||||
}
|
||||
else if ([string hasPrefix:kMXKSlashCmdJoinRoom])
|
||||
|
@ -1355,7 +1360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// Display cmd usage in text input as placeholder
|
||||
cmdUsage = @"Usage: /join <room_alias>";
|
||||
cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
|
||||
}
|
||||
}
|
||||
else if ([string hasPrefix:kMXKSlashCmdPartRoom])
|
||||
|
@ -1413,7 +1418,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// Display cmd usage in text input as placeholder
|
||||
cmdUsage = @"Usage: /part [<room_alias>]";
|
||||
cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom];
|
||||
}
|
||||
}
|
||||
else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic])
|
||||
|
@ -1445,10 +1450,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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:^{
|
||||
MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session");
|
||||
|
@ -1470,7 +1475,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
userId = nil;
|
||||
}
|
||||
|
||||
if ([cmd isEqualToString:kMXKSlashCmdInviteUser])
|
||||
if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]])
|
||||
{
|
||||
if (userId)
|
||||
{
|
||||
|
@ -1489,10 +1494,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
@ -1524,10 +1529,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
@ -1559,10 +1564,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
@ -1581,10 +1586,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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
|
||||
NSString *powerLevel = nil;
|
||||
|
@ -1617,10 +1622,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
@ -1639,7 +1644,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
|||
else
|
||||
{
|
||||
// Display cmd usage in text input as placeholder
|
||||
cmdUsage = @"Usage: /deop <userId>";
|
||||
cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel];
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -61,7 +61,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
|
|||
// The preview header
|
||||
@property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer;
|
||||
@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
|
||||
@property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer;
|
||||
|
|
|
@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
|
||||
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
|
||||
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
|
||||
|
@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
@property (nonatomic, strong) ShareManager *shareManager;
|
||||
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
|
||||
|
||||
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
|
||||
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
|
||||
@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
|
||||
@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;
|
||||
|
||||
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
|
||||
|
||||
|
@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
|
||||
[self setupActions];
|
||||
|
||||
[self setupUserSuggestionViewIfNeeded];
|
||||
[self setupCompletionSuggestionViewIfNeeded];
|
||||
|
||||
[self.topBannersStackView vc_removeAllSubviews];
|
||||
}
|
||||
|
@ -693,7 +693,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
{
|
||||
// Retrieve the potential message partially typed during last room display.
|
||||
// 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];
|
||||
|
@ -1088,12 +1088,14 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
|
||||
_voiceMessageController.roomId = dataSource.roomId;
|
||||
|
||||
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
|
||||
_completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
|
||||
room:dataSource.room
|
||||
userID:self.roomDataSource.mxSession.myUserId];
|
||||
_userSuggestionCoordinator.delegate = self;
|
||||
_completionSuggestionCoordinator.delegate = self;
|
||||
|
||||
[self setupUserSuggestionViewIfNeeded];
|
||||
[self setupCompletionSuggestionViewIfNeeded];
|
||||
|
||||
[self updateRoomInputToolbarViewClassIfNeeded];
|
||||
|
||||
[self updateTopBanners];
|
||||
}
|
||||
|
@ -1195,6 +1197,12 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
{
|
||||
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;
|
||||
|
||||
// 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
|
||||
|
||||
NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
|
||||
|
||||
if ([string hasPrefix:kMXKSlashCmdJoinRoom])
|
||||
{
|
||||
// Join a room
|
||||
|
@ -1317,7 +1327,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
else
|
||||
{
|
||||
// Display cmd usage in text input as placeholder
|
||||
self.inputToolbarView.placeholder = @"Usage: /join <room_alias>";
|
||||
self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
@ -2726,13 +2736,13 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)setupUserSuggestionViewIfNeeded
|
||||
- (void)setupCompletionSuggestionViewIfNeeded
|
||||
{
|
||||
if(!self.isViewLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable;
|
||||
UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
|
||||
|
||||
if (!suggestionsViewController)
|
||||
{
|
||||
|
@ -2742,12 +2752,12 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
||||
[self addChildViewController:suggestionsViewController];
|
||||
[self.userSuggestionContainerView addSubview:suggestionsViewController.view];
|
||||
[self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor],
|
||||
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor],
|
||||
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor],
|
||||
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]];
|
||||
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
|
||||
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
|
||||
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
|
||||
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
|
||||
|
||||
[suggestionsViewController didMoveToParentViewController:self];
|
||||
}
|
||||
|
@ -5147,17 +5157,17 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
|
||||
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
|
||||
{
|
||||
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage];
|
||||
[self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
|
||||
}
|
||||
|
||||
- (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
|
||||
|
@ -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
|
||||
{
|
||||
NSMutableArray *actionItems = [NSMutableArray new];
|
||||
|
@ -5237,7 +5268,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
if (readyToSend) {
|
||||
BOOL isMessageAHandledCommand = NO;
|
||||
// "/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).
|
||||
NSString *message;
|
||||
|
@ -5262,6 +5293,11 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage
|
||||
{
|
||||
self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage;
|
||||
}
|
||||
|
||||
#pragma mark - MXKRoomMemberDetailsViewControllerDelegate
|
||||
|
||||
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion
|
||||
|
@ -6104,7 +6140,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
if (self.saveProgressTextInput)
|
||||
{
|
||||
// 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;
|
||||
|
||||
// Acknowledge the existence of all devices
|
||||
[self startActivityIndicator];
|
||||
self->unknownDevices = nil;
|
||||
|
||||
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 stopActivityIndicator];
|
||||
|
||||
// And resend pending messages
|
||||
[self resendAllUnsentMessages];
|
||||
}];
|
||||
// And resend pending messages
|
||||
[self resendAllUnsentMessages];
|
||||
}
|
||||
|
||||
}]];
|
||||
|
@ -7484,23 +7509,47 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
return;
|
||||
}
|
||||
|
||||
NSMutableArray<NSIndexPath *> *rowsToReload = [[NSMutableArray alloc] init];
|
||||
// Get the current hightlighted event because we will need to reload it
|
||||
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])
|
||||
{
|
||||
[rowsToReload addObject:indexPath];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.customizedRoomDataSource.highlightedEventId = eventId;
|
||||
|
||||
// Add the new highligted event to the list of rows to reload
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
|
||||
if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
|
||||
BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath];
|
||||
if (indexPathIsVisible)
|
||||
{
|
||||
[self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
[rowsToReload addObject:indexPath];
|
||||
}
|
||||
|
||||
// Reload rows
|
||||
if (rowsToReload.count > 0)
|
||||
{
|
||||
[self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.bubblesTableView scrollToRowAtIndexPath:indexPath
|
||||
atScrollPosition:UITableViewScrollPositionMiddle
|
||||
animated:YES];
|
||||
}
|
||||
else if ([self.bubblesTableView vc_hasIndexPath:indexPath])
|
||||
|
||||
// Scroll to the newly highlighted row
|
||||
if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath])
|
||||
{
|
||||
[self.bubblesTableView scrollToRowAtIndexPath:indexPath
|
||||
atScrollPosition:UITableViewScrollPositionMiddle
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
if (completion)
|
||||
{
|
||||
completion();
|
||||
|
@ -8070,9 +8119,9 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
|
||||
}
|
||||
|
||||
#pragma mark - UserSuggestionCoordinatorBridgeDelegate
|
||||
#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate
|
||||
|
||||
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator
|
||||
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
|
||||
didRequestMentionForMember:(MXRoomMember *)member
|
||||
textTrigger:(NSString *)textTrigger
|
||||
{
|
||||
|
@ -8080,16 +8129,32 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
[self mention:member];
|
||||
}
|
||||
|
||||
- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator
|
||||
- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
|
||||
textTrigger:(NSString *)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
|
||||
{
|
||||
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) {
|
||||
NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage];
|
||||
[[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];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
@ -91,7 +107,7 @@ extension RoomViewController {
|
|||
"event_id": eventModified.eventId
|
||||
])
|
||||
})
|
||||
} else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) {
|
||||
} else {
|
||||
roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in
|
||||
switch response {
|
||||
case .success:
|
||||
|
|
|
@ -1,9 +1,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"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<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"/>
|
||||
|
@ -13,6 +12,8 @@
|
|||
<connections>
|
||||
<outlet property="bubblesTableView" destination="BGD-sd-SQR" id="OG4-Tw-Ovt"/>
|
||||
<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="jumpToLastUnreadBanner" destination="S6r-bo-jxw" id="FSS-Be-E15"/>
|
||||
<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="scrollToBottomButton" destination="Ih9-EU-BOU" id="Wwg-gS-Sfp"/>
|
||||
<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"/>
|
||||
<outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/>
|
||||
<outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/>
|
||||
|
@ -48,20 +47,20 @@
|
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<constraint firstAttribute="height" priority="250" id="Y9P-Ek-wjg"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<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"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="RoomVCBubblesTableView"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableView>
|
||||
<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"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="RoomVCPreviewHeaderContainer"/>
|
||||
<constraints>
|
||||
|
@ -69,7 +68,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<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>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hB3-nR-MVR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="54"/>
|
||||
|
@ -189,7 +188,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<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"/>
|
||||
</view>
|
||||
</subviews>
|
||||
|
@ -237,11 +236,6 @@
|
|||
<point key="canvasLocation" x="136.80000000000001" y="152.47376311844079"/>
|
||||
</view>
|
||||
</objects>
|
||||
<designables>
|
||||
<designable name="QHs-rM-UU8">
|
||||
<size key="intrinsicContentSize" width="7.5" height="13.5"/>
|
||||
</designable>
|
||||
</designables>
|
||||
<resources>
|
||||
<image name="new_close" width="16" height="16"/>
|
||||
<image name="room_scroll_up" width="24" height="24"/>
|
||||
|
|
|
@ -164,6 +164,11 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell {
|
|||
roomCellContentView.didTapAddParticipants = { [weak self] in
|
||||
self?.notifyDelegate(with: RoomCreationIntroCell.tapOnAddParticipants)
|
||||
}
|
||||
|
||||
self.accessibilityElements = [roomCellContentView.roomAvatarView as Any,
|
||||
roomCellContentView.titleLabel as Any,
|
||||
roomCellContentView.informationLabel as Any,
|
||||
roomCellContentView.addParticipantsContainerView as Any]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -69,8 +69,10 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable {
|
|||
|
||||
self.addParticipantsButton.layer.masksToBounds = true
|
||||
self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside)
|
||||
self.addParticipantsButton.accessibilityLabel = VectorL10n.roomIntroCellAddParticipantsAction
|
||||
|
||||
self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction
|
||||
self.addParticipantsLabel.isAccessibilityElement = false
|
||||
|
||||
self.roomAvatarView.showCameraBadgeOnFallbackImage = true
|
||||
}
|
||||
|
|
|
@ -35,6 +35,15 @@ class TextMessageOutgoingWithoutSenderInfoBubbleCell: TextMessageBaseBubbleCell,
|
|||
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
|
||||
|
||||
private func setupBubbleConstraints() {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
class RoomInputToolbarTextView: UITextView {
|
||||
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
private var pillViews = [UIView]()
|
||||
|
||||
weak var toolbarDelegate: RoomInputToolbarTextViewDelegate?
|
||||
|
||||
|
@ -51,12 +52,18 @@ class RoomInputToolbarTextView: UITextView {
|
|||
}
|
||||
|
||||
override var text: String! {
|
||||
willSet {
|
||||
flushPills()
|
||||
}
|
||||
didSet {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
||||
override var attributedText: NSAttributedString! {
|
||||
willSet {
|
||||
flushPills()
|
||||
}
|
||||
didSet {
|
||||
updateUI()
|
||||
}
|
||||
|
@ -162,3 +169,17 @@ class RoomInputToolbarTextView: UITextView {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
@class RoomInputToolbarView;
|
||||
@class LinkActionWrapper;
|
||||
@class SuggestionPatternWrapper;
|
||||
@class UserSuggestionViewModelContextWrapper;
|
||||
@class CompletionSuggestionViewModelContextWrapper;
|
||||
|
||||
/**
|
||||
Destination of the message in the composer
|
||||
|
@ -84,7 +84,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
|
|||
|
||||
- (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern;
|
||||
|
||||
- (UserSuggestionViewModelContextWrapper *)userSuggestionContext;
|
||||
- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext;
|
||||
|
||||
- (MXMediaManager *)mediaManager;
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
|||
|
||||
_sendMode = RoomInputToolbarViewSendModeSend;
|
||||
self.inputContextViewHeightConstraint.constant = 0;
|
||||
self.inputContextLabel.isAccessibilityElement = NO;
|
||||
self.inputContextButton.isAccessibilityElement = NO;
|
||||
|
||||
[self.rightInputToolbarButton setTitle:nil forState:UIControlStateNormal];
|
||||
[self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted];
|
||||
|
@ -252,6 +254,10 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
|||
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];
|
||||
|
||||
if (self.maxHeight && updatedHeight > self.maxHeight)
|
||||
|
@ -477,11 +483,22 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
|||
[self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor],
|
||||
[self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor],
|
||||
[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
|
||||
{
|
||||
[self.voiceMessageToolbarView removeFromSuperview];
|
||||
_voiceMessageToolbarView = nil;
|
||||
self.accessibilityElements = nil;
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -96,11 +96,17 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
|||
// Note: this is only interactive in plain text mode. If RTE is enabled,
|
||||
// APIs from the composer view model should be used.
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
self.viewModel.showKeyboard()
|
||||
}
|
||||
|
@ -191,11 +207,15 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
|||
}
|
||||
|
||||
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,
|
||||
mentionType: .user)
|
||||
}
|
||||
|
||||
func command(_ command: String) {
|
||||
self.wysiwygViewModel.setCommand(name: command)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupComposerIfNeeded() {
|
||||
|
@ -219,7 +239,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
|||
let composer = Composer(
|
||||
viewModel: viewModel.context,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context,
|
||||
completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context,
|
||||
resizeAnimationDuration: Double(kResizeComposerAnimationDuration),
|
||||
sendMessageAction: { [weak self] content in
|
||||
guard let self = self else { return }
|
||||
|
@ -277,12 +297,31 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
|||
},
|
||||
|
||||
wysiwygViewModel.$plainTextContent
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] value in
|
||||
guard let self else { return }
|
||||
self.textMessage = value.string
|
||||
.dropFirst()
|
||||
.sink { [weak self] attributed in
|
||||
// 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?.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) {
|
||||
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown)
|
||||
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)
|
||||
}
|
||||
|
||||
if isMaximised {
|
||||
minimise()
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -121,11 +121,12 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
|
|||
private func showSecureBackupSetup(checkKeyBackup: Bool) {
|
||||
let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable)
|
||||
coordinator.delegate = self
|
||||
coordinator.start()
|
||||
|
||||
self.navigationRouter.push(coordinator.toPresentable(), animated: true, popCompletion: { [weak self] in
|
||||
// Fix: calling coordinator.start() will update the navigationRouter without a popCompletion
|
||||
coordinator.start(popCompletion: { [weak self] in
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,11 +94,11 @@ extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate {
|
|||
extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate {
|
||||
|
||||
func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) {
|
||||
|
||||
self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:]))
|
||||
}
|
||||
|
||||
func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) {
|
||||
self.secretsResetViewModel.process(viewAction: .authenticationCancelled)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import Foundation
|
|||
enum SecretsResetViewAction {
|
||||
case loadData
|
||||
case reset
|
||||
case authenticationCancelled
|
||||
case authenticationInfoEntered(_ authInfo: [String: Any])
|
||||
case cancel
|
||||
}
|
||||
|
|
|
@ -132,6 +132,8 @@ final class SecretsResetViewController: UIViewController {
|
|||
self.renderLoading()
|
||||
case .resetDone:
|
||||
self.renderLoaded()
|
||||
case .resetCancelled:
|
||||
self.renderCancelled()
|
||||
case .error(let error):
|
||||
self.render(error: error)
|
||||
}
|
||||
|
@ -145,6 +147,10 @@ final class SecretsResetViewController: UIViewController {
|
|||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
}
|
||||
|
||||
private func renderCancelled() {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
}
|
||||
|
||||
private func render(error: Error) {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
|
||||
|
|
|
@ -49,6 +49,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
|
|||
break
|
||||
case .reset:
|
||||
self.askAuthentication()
|
||||
case .authenticationCancelled:
|
||||
self.authenticationCancelled()
|
||||
case .authenticationInfoEntered(let authParameters):
|
||||
self.resetSecrets(with: authParameters)
|
||||
case .cancel:
|
||||
|
@ -68,7 +70,6 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
|
|||
}
|
||||
MXLog.debug("[SecretsResetViewModel] resetSecrets")
|
||||
|
||||
self.update(viewState: .resetting)
|
||||
crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
|
@ -96,7 +97,13 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
|
|||
}
|
||||
|
||||
private func askAuthentication() {
|
||||
self.update(viewState: .resetting)
|
||||
|
||||
let setupCrossSigningRequest = self.crossSigningService.setupCrossSigningRequest()
|
||||
self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: setupCrossSigningRequest)
|
||||
}
|
||||
|
||||
private func authenticationCancelled() {
|
||||
self.update(viewState: .resetCancelled)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@ import Foundation
|
|||
enum SecretsResetViewState {
|
||||
case resetting
|
||||
case resetDone
|
||||
case resetCancelled
|
||||
case error(Error)
|
||||
}
|
||||
|
|
|
@ -73,12 +73,16 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
|
|||
// MARK: - Public methods
|
||||
|
||||
func start() {
|
||||
start(popCompletion: nil)
|
||||
}
|
||||
|
||||
func start(popCompletion: (() -> Void)?) {
|
||||
let rootViewController = self.createIntro()
|
||||
|
||||
if self.navigationRouter.modules.isEmpty == false {
|
||||
self.navigationRouter.push(rootViewController, animated: true, popCompletion: nil)
|
||||
self.navigationRouter.push(rootViewController, animated: true, popCompletion: popCompletion)
|
||||
} else {
|
||||
self.navigationRouter.setRootModule(rootViewController)
|
||||
self.navigationRouter.setRootModule(rootViewController, popCompletion: popCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
<?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"/>
|
||||
<dependencies>
|
||||
<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="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Enter Pin Code View Controller-->
|
||||
<scene sceneID="mt5-wz-YKA">
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<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>
|
||||
<constraint firstAttribute="width" constant="100" id="Ux9-pH-SW9"/>
|
||||
<constraint firstAttribute="height" constant="100" id="o3K-YH-5tn"/>
|
||||
|
@ -34,7 +35,7 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<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>
|
||||
<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"/>
|
||||
|
@ -44,20 +45,20 @@
|
|||
</constraints>
|
||||
</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">
|
||||
<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"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<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>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<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>
|
||||
<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"/>
|
||||
|
@ -97,7 +98,7 @@
|
|||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Q0w-RD-JD3">
|
||||
<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>
|
||||
<constraint firstAttribute="height" constant="2" id="thx-rI-kOC"/>
|
||||
</constraints>
|
||||
|
@ -106,7 +107,7 @@
|
|||
<rect key="frame" x="8" y="18" width="355" height="29"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<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"/>
|
||||
</label>
|
||||
</subviews>
|
||||
|
@ -124,12 +125,12 @@
|
|||
</subviews>
|
||||
</stackView>
|
||||
<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>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="72" width="220" height="60"/>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="144" width="220" height="60"/>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="216" width="220" height="60"/>
|
||||
<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"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="NO"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="60" id="fVd-IS-maA"/>
|
||||
<constraint firstAttribute="height" constant="60" id="x1Z-Ar-22U"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/>
|
||||
</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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="60" id="DDy-yf-FOR"/>
|
||||
|
@ -288,8 +292,8 @@
|
|||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CRt-Fb-0Dq">
|
||||
<rect key="frame" x="146.5" y="598" width="82" height="33"/>
|
||||
<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="581" width="82" height="33"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<state key="normal" title="Forgot PIN"/>
|
||||
<connections>
|
||||
|
@ -297,7 +301,7 @@
|
|||
</connections>
|
||||
</button>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="oFX-h1-fx7"/>
|
||||
|
@ -312,6 +316,7 @@
|
|||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
|
||||
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<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="1YE-D1-eHn" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="qOT-0t-zQs"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
|
||||
</view>
|
||||
<connections>
|
||||
<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="back_icon" width="14" height="23"/>
|
||||
<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>
|
||||
</document>
|
||||
|
|
|
@ -91,6 +91,10 @@ final class SetPinCoordinatorBridgePresenter: NSObject {
|
|||
}
|
||||
|
||||
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 setPinCoordinator = SetPinCoordinator(session: self.session, viewMode: self.viewMode, pinCodePreferences: .shared)
|
||||
|
|
|
@ -176,8 +176,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
|
|||
LABS_ENABLE_NEW_SESSION_MANAGER,
|
||||
LABS_ENABLE_NEW_CLIENT_INFO_FEATURE,
|
||||
LABS_ENABLE_WYSIWYG_COMPOSER,
|
||||
LABS_ENABLE_VOICE_BROADCAST,
|
||||
LABS_ENABLE_CRYPTO_SDK
|
||||
LABS_ENABLE_VOICE_BROADCAST
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, SECURITY)
|
||||
|
@ -588,11 +587,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
|||
if (BuildSettings.settingsScreenShowLabSettings)
|
||||
{
|
||||
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_THREADS_INDEX];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS];
|
||||
|
@ -2587,18 +2581,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
|||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
@ -3372,30 +3354,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
|||
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
|
||||
{
|
||||
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
|
||||
|
@ -4200,6 +4158,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
|||
|| (language == nil && [NSBundle mxk_language]))
|
||||
{
|
||||
[NSBundle mxk_setLanguage:language];
|
||||
UIApplication.sharedApplication.accessibilityLanguage = language;
|
||||
|
||||
// Store user settings
|
||||
NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;
|
||||
|
|
|
@ -35,7 +35,7 @@ class ThreadTableViewCell: UITableViewCell {
|
|||
|
||||
@IBOutlet private weak var rootMessageAvatarView: UserAvatarView!
|
||||
@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 summaryView: ThreadSummaryView!
|
||||
@IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView!
|
||||
|
@ -61,7 +61,7 @@ class ThreadTableViewCell: UITableViewCell {
|
|||
if let rootMessageText = model.rootMessageText {
|
||||
updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor)
|
||||
} else {
|
||||
rootMessageContentLabel.attributedText = nil
|
||||
rootMessageContentTextView.attributedText = nil
|
||||
}
|
||||
lastMessageTimeLabel.text = model.lastMessageTime
|
||||
if let summaryModel = model.summaryModel {
|
||||
|
@ -83,7 +83,7 @@ class ThreadTableViewCell: UITableViewCell {
|
|||
mutable.addAttributes([
|
||||
.foregroundColor: color
|
||||
], 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)
|
||||
updateRootMessageSenderColor()
|
||||
rootMessageAvatarView.backgroundColor = .clear
|
||||
if let attributedText = rootMessageContentLabel.attributedText {
|
||||
if let attributedText = rootMessageContentTextView.attributedText {
|
||||
updateRootMessageContentAttributes(attributedText, color: rootMessageColor)
|
||||
}
|
||||
lastMessageTimeLabel.textColor = theme.colors.secondaryContent
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?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"/>
|
||||
<dependencies>
|
||||
<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="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -11,14 +11,14 @@
|
|||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<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"/>
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<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"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</constraints>
|
||||
</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">
|
||||
<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"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -38,7 +38,7 @@
|
|||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</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"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
|
@ -51,13 +51,13 @@
|
|||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</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">
|
||||
<rect key="frame" x="56" y="33" width="236" height="17"/>
|
||||
<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="21" width="236" height="33"/>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Element" customModuleProvider="target">
|
||||
<rect key="frame" x="44" y="54" width="264" height="32"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
|
@ -68,20 +68,20 @@
|
|||
<constraints>
|
||||
<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="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 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 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 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="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="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 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 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>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
|
@ -89,7 +89,7 @@
|
|||
<outlet property="lastMessageTimeLabel" destination="C2U-Ih-4Oh" id="pf3-df-T65"/>
|
||||
<outlet property="notificationStatusView" destination="aUq-D2-1KM" id="IDB-Yf-weu"/>
|
||||
<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="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/>
|
||||
</connections>
|
||||
|
@ -97,6 +97,9 @@
|
|||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="labelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
|
|
@ -273,22 +273,12 @@
|
|||
- (IBAction)onDone:(id)sender
|
||||
{
|
||||
// Acknowledge the existence of all devices before leaving this screen
|
||||
[self startActivityIndicator];
|
||||
if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
|
||||
if (self->onCompleteBlock)
|
||||
{
|
||||
MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices");
|
||||
return;
|
||||
self->onCompleteBlock(YES);
|
||||
}
|
||||
[(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{
|
||||
|
||||
[self stopActivityIndicator];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
|
||||
if (self->onCompleteBlock)
|
||||
{
|
||||
self->onCompleteBlock(YES);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (IBAction)onCancel:(id)sender
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#import "RoomBubbleCellData.h"
|
||||
#import "MXKRoomBubbleTableViewCell+Riot.h"
|
||||
#import "UserEncryptionTrustLevel.h"
|
||||
#import "RoomEncryptionTrustLevel.h"
|
||||
#import "RoomReactionsViewSizer.h"
|
||||
#import "RoomEncryptedDataBubbleCell.h"
|
||||
#import "LegacyAppDelegate.h"
|
||||
|
|
|
@ -573,8 +573,13 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
|
|||
{
|
||||
// Force the default text color for the last message (cancel highlighted message color)
|
||||
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText];
|
||||
[lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor
|
||||
range:NSMakeRange(0, lastEventDescription.length)];
|
||||
NSRange 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;
|
||||
}
|
||||
|
||||
|
@ -670,9 +675,11 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
|
|||
|
||||
NSAttributedString *attachmentString = nil;
|
||||
UIColor *textColor;
|
||||
NSString *colorIdentifier;
|
||||
if (isStoppedVoiceBroadcast)
|
||||
{
|
||||
textColor = ThemeService.shared.theme.textSecondaryColor;
|
||||
textColor = ThemeService.shared.theme.colors.secondaryContent;
|
||||
colorIdentifier = @"secondaryContent";
|
||||
NSString *senderDisplayName;
|
||||
if ([stateEvent.stateKey isEqualToString:session.myUser.userId])
|
||||
{
|
||||
|
@ -688,6 +695,7 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
|
|||
else
|
||||
{
|
||||
textColor = ThemeService.shared.theme.colors.alert;
|
||||
colorIdentifier = @"alert";
|
||||
UIImage *liveImage = AssetImages.voiceBroadcastLive.image;
|
||||
|
||||
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
|
||||
|
@ -717,6 +725,12 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
|
|||
}
|
||||
|
||||
[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;
|
||||
|
||||
return YES;
|
||||
|
|
48
Riot/Utils/ThemeColorResolver.swift
Normal file
48
Riot/Utils/ThemeColorResolver.swift
Normal 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]
|
||||
}
|
||||
}
|
|
@ -41,7 +41,6 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
private var ongoingVoIPPushRequests: [String: Bool] = [:]
|
||||
|
||||
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
|
||||
private var bestAttemptContents: [String: UNMutableNotificationContent] = [:]
|
||||
|
@ -196,13 +195,12 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
self.userAccount = MXKAccountManager.shared()?.activeAccounts.first
|
||||
if let userAccount = userAccount {
|
||||
Self.backgroundServiceInitQueue.sync {
|
||||
if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials {
|
||||
if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials {
|
||||
MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE")
|
||||
self.logMemory()
|
||||
|
||||
NotificationService.backgroundSyncService = MXBackgroundSyncService(
|
||||
withCredentials: userAccount.mxCredentials,
|
||||
isCryptoSDKEnabled: isCryptoSDKEnabled,
|
||||
persistTokenDataHandler: { persistTokenDataHandler in
|
||||
MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler)
|
||||
}, 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
|
||||
/// - Parameters:
|
||||
/// - eventId: Event identifier to mutate best attempt content
|
||||
|
|
|
@ -102,11 +102,6 @@ static MXSession *fakeSession;
|
|||
[session setStore:self.fileStore success:^{
|
||||
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];
|
||||
for (NSString *roomIdentifier in roomIdentifiers) {
|
||||
MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session];
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#import "AvatarGenerator.h"
|
||||
#import "BuildInfo.h"
|
||||
#import "ShareItemSender.h"
|
||||
#import "UserEncryptionTrustLevel.h"
|
||||
#import "RoomEncryptionTrustLevel.h"
|
||||
|
||||
// MatrixKit imports
|
||||
#import "MatrixKit-Bridging-Header.h"
|
||||
|
|
|
@ -87,3 +87,4 @@ targets:
|
|||
- "**/*.md" # excludes all files with the .md extension
|
||||
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
|
||||
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
|
||||
- path: ../Riot/Modules/Encryption/EncryptionTrustLevel.swift
|
||||
|
|
|
@ -86,6 +86,10 @@ class HomeserverAddress: NSObject {
|
|||
/// - Ensure the address contains a scheme, otherwise make it `https`.
|
||||
/// - Remove any trailing slashes.
|
||||
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()
|
||||
|
||||
if !address.contains("://") {
|
||||
|
|
|
@ -267,17 +267,6 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
|||
|
||||
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
|
||||
session.enableCrypto(true) { response in
|
||||
continuation.resume(returning: response)
|
||||
|
|
|
@ -57,7 +57,13 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
|
|||
self.parameters = parameters
|
||||
|
||||
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,
|
||||
hasModalPresentation: parameters.hasModalPresentation)
|
||||
let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context)
|
||||
|
|
|
@ -51,7 +51,7 @@ enum MockAppScreens {
|
|||
MockStaticLocationViewingScreenState.self,
|
||||
MockLocationSharingScreenState.self,
|
||||
MockAnalyticsPromptScreenState.self,
|
||||
MockUserSuggestionScreenState.self,
|
||||
MockCompletionSuggestionScreenState.self,
|
||||
MockPollEditFormScreenState.self,
|
||||
MockSpaceCreationEmailInvitesScreenState.self,
|
||||
MockSpaceSettingsScreenState.self,
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
protocol UserSuggestionViewModelProtocol {
|
||||
/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple
|
||||
/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController`
|
||||
protocol CompletionSuggestionViewModelProtocol {
|
||||
/// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple
|
||||
/// `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.
|
||||
var sharedContext: UserSuggestionViewModelType.Context { get }
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
|
||||
var sharedContext: CompletionSuggestionViewModelType.Context { get }
|
||||
var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -18,14 +18,25 @@ import Combine
|
|||
import Foundation
|
||||
import WysiwygComposer
|
||||
|
||||
protocol UserSuggestionItemProtocol: Avatarable {
|
||||
protocol CompletionSuggestionUserItemProtocol: Avatarable {
|
||||
var userId: String { get }
|
||||
var displayName: String? { get }
|
||||
var avatarUrl: String? { get }
|
||||
}
|
||||
|
||||
protocol UserSuggestionServiceProtocol {
|
||||
var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get }
|
||||
protocol CompletionSuggestionCommandItemProtocol {
|
||||
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 }
|
||||
|
||||
|
@ -35,7 +46,7 @@ protocol UserSuggestionServiceProtocol {
|
|||
|
||||
// MARK: Avatarable
|
||||
|
||||
extension UserSuggestionItemProtocol {
|
||||
extension CompletionSuggestionUserItemProtocol {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
}
|
|
@ -17,9 +17,9 @@
|
|||
import RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
class UserSuggestionUITests: MockScreenTestCase {
|
||||
func testUserSuggestionScreen() throws {
|
||||
app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title)
|
||||
class CompletionSuggestionUITests: MockScreenTestCase {
|
||||
func testCompletionSuggestionScreen() throws {
|
||||
app.goToScreenWithIdentifier(MockCompletionSuggestionScreenState.multipleResults.title)
|
||||
|
||||
let firstButton = app.buttons["displayNameText-userIdText"].firstMatch
|
||||
XCTAssert(firstButton.waitForExistence(timeout: 10))
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue