diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 68a169237..24e8ab2e6 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -11,7 +11,6 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} - MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 3fcc19ed3..00d4fa2f9 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -12,7 +12,6 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} - MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: tests: diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index 7ab8b331d..dfbd7ac6a 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -11,7 +11,6 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} - MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 44704833b..320360fd5 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -5,6 +5,31 @@ on: types: [labeled] jobs: + apply_Z-Labs_label: + name: Add Z-Labs label for features behind labs flags + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'A-Maths') || + contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || + contains(github.event.issue.labels.*.name, 'A-Threads') || + contains(github.event.issue.labels.*.name, 'A-Polls') || + contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || + contains(github.event.issue.labels.*.name, 'Z-IA') || + contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || + contains(github.event.issue.labels.*.name, 'A-Tags') + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['Z-Labs'] + }) + move_needs_info_issues: name: X-Needs-Info issues to Need info column on triage board runs-on: ubuntu-latest @@ -34,7 +59,7 @@ jobs: with: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | - mutation add_to_project($projectid:String!,$contentid:String!) { + mutation add_to_project($projectid:ID!,$contentid:ID!) { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { projectNextItem { id @@ -47,19 +72,44 @@ jobs: PROJECT_ID: "PN_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + add_product_issues_to_project: + name: X-Needs-Product to Design project board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'X-Needs-Product') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_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, 'A-Spaces') || contains(github.event.issue.labels.*.name, 'A-Space-Settings') || - contains(github.event.issue.labels.*.name, 'A-Subspaces') + contains(github.event.issue.labels.*.name, 'A-Subspaces') || + contains(github.event.issue.labels.*.name, 'Z-IA') steps: - uses: octokit/graphql-action@v2.x with: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | - mutation add_to_project($projectid:String!,$contentid:String!) { + mutation add_to_project($projectid:ID!,$contentid:ID!) { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { projectNextItem { id @@ -82,7 +132,7 @@ jobs: with: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | - mutation add_to_project($projectid:String!,$contentid:String!) { + mutation add_to_project($projectid:ID!,$contentid:ID!) { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { projectNextItem { id @@ -105,7 +155,7 @@ jobs: with: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | - mutation add_to_project($projectid:String!,$contentid:String!) { + mutation add_to_project($projectid:ID!,$contentid:ID!) { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { projectNextItem { id @@ -128,7 +178,7 @@ jobs: with: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | - mutation add_to_project($projectid:String!,$contentid:String!) { + mutation add_to_project($projectid:ID!,$contentid:ID!) { addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { projectNextItem { id diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-move-unlabelled.yml index 94bd049b9..453fafe54 100644 --- a/.github/workflows/triage-move-unlabelled.yml +++ b/.github/workflows/triage-move-unlabelled.yml @@ -33,3 +33,29 @@ jobs: project: Issue triage column: Triaged repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + remove_Z-Labs_label: + name: Remove Z-Labs label when features behind labs flags are removed + runs-on: ubuntu-latest + if: > + !(contains(github.event.issue.labels.*.name, 'A-Maths') || + contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || + contains(github.event.issue.labels.*.name, 'A-Threads') || + contains(github.event.issue.labels.*.name, 'A-Polls') || + contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || + contains(github.event.issue.labels.*.name, 'Z-IA') || + contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || + contains(github.event.issue.labels.*.name, 'A-Tags')) && + contains(github.event.issue.labels.*.name, 'Z-Labs') + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['Z-Labs'] + }) diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index 0a4d1c49f..843c6234c 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -34,6 +34,7 @@ jobs: P1_issues_to_crypto_team_workboard: runs-on: ubuntu-latest if: > + contains(github.event.issue.labels.*.name, 'Z-UISI') || (contains(github.event.issue.labels.*.name, 'A-E2EE') || contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || diff --git a/.github/workflows/triage-review-requests.yml b/.github/workflows/triage-review-requests.yml new file mode 100644 index 000000000..8306cffef --- /dev/null +++ b/.github/workflows/triage-review-requests.yml @@ -0,0 +1,139 @@ +name: Move pull requests asking for review to the relevant project +on: + pull_request_target: + types: [review_requested] + +jobs: + add_design_pr_to_project: + name: Move PRs asking for design review to the design board + runs-on: ubuntu-latest + steps: + - uses: octokit/graphql-action@v2.x + id: find_team_members + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + query find_team_members($team: String!) { + organization(login: "vector-im") { + team(slug: $team) { + members { + nodes { + login + } + } + } + } + } + team: ${{ env.TEAM }} + env: + TEAM: "design" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + - id: any_matching_reviewers + run: | + # Fetch requested reviewers, and people who are on the team + echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json + echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json + jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt + jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt + + # Fetch requested team reviewers, and the name of the team + echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json + jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt + echo '${{ env.TEAM }}' | tee /tmp/team.txt + + # If either a reviewer matches a team member, or a team matches our team, say "true" + if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + else + echo "::set-output name=match::false" + fi + env: + TEAM: "design" + - uses: octokit/graphql-action@v2.x + id: add_to_project + if: steps.any_matching_reviewers.outputs.match == 'true' + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!, $contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.pull_request.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc0sUA" + TEAM: "design" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + add_product_pr_to_project: + name: Move PRs asking for design review to the design board + runs-on: ubuntu-latest + steps: + - uses: octokit/graphql-action@v2.x + id: find_team_members + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + query find_team_members($team: String!) { + organization(login: "vector-im") { + team(slug: $team) { + members { + nodes { + login + } + } + } + } + } + team: ${{ env.TEAM }} + env: + TEAM: "product" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + - id: any_matching_reviewers + run: | + # Fetch requested reviewers, and people who are on the team + echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json + echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json + jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt + jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt + + # Fetch requested team reviewers, and the name of the team + echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json + jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt + echo '${{ env.TEAM }}' | tee /tmp/team.txt + + # If either a reviewer matches a team member, or a team matches our team, say "true" + if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + else + echo "::set-output name=match::false" + fi + env: + TEAM: "product" + - uses: octokit/graphql-action@v2.x + id: add_to_project + if: steps.any_matching_reviewers.outputs.match == 'true' + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!, $contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.pull_request.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + TEAM: "product" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 7430599ac..cac537057 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -15,7 +15,6 @@ // import Foundation -import Keys /// BuildSettings provides settings computed at build time. /// In future, it may be automatically generated from xcconfig files @@ -213,8 +212,10 @@ final class BuildSettings: NSObject { static let allowInviteExernalUsers: Bool = true + // MARK: - Side Menu static let enableSideMenu: Bool = true - + static let sideMenuShowInviteFriends: Bool = true + /// Whether to read the `io.element.functional_members` state event and exclude any service members when computing a room's name and avatar. static let supportFunctionalMembers: Bool = true @@ -261,7 +262,6 @@ final class BuildSettings: NSObject { static let settingsScreenAllowBugReportingManually: Bool = true static let settingsScreenAllowDeactivatingAccount: Bool = true static let settingsScreenShowChangePassword:Bool = true - static let settingsScreenShowInviteFriends:Bool = true static let settingsScreenShowEnableStunServerFallback: Bool = true static let settingsScreenShowNotificationDecodedContentOption: Bool = true static let settingsScreenShowNsfwRoomsOption: Bool = true @@ -349,6 +349,7 @@ final class BuildSettings: NSObject { static let authScreenShowPhoneNumber = true static let authScreenShowForgotPassword = true static let authScreenShowCustomServerOptions = true + static let authScreenShowSocialLoginSection = true // MARK: - Authentication Options static let authEnableRefreshTokens = true @@ -371,13 +372,13 @@ final class BuildSettings: NSObject { // MARK: - Location Sharing - static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! static var locationSharingEnabled: Bool { guard #available(iOS 14, *) else { return false } - return false + return true } } diff --git a/Gemfile b/Gemfile index ea061a17e..53efbaf92 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' -gem "cocoapods-keys" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 78fe028a3..610cbbadd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,6 @@ GEM specs: CFPropertyList (3.0.5) rexml - RubyInline (3.12.5) - ZenTest (~> 4.3) - ZenTest (4.12.0) activesupport (6.1.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -67,9 +64,6 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) - cocoapods-keys (2.2.1) - dotenv - osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -231,8 +225,6 @@ GEM netrc (0.11.0) optparse (0.1.1) os (1.1.4) - osx_keychain (1.0.2) - RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -300,7 +292,6 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.11.2) - cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning @@ -308,4 +299,4 @@ DEPENDENCIES xcode-install BUNDLED WITH - 2.2.28 + 2.2.32 diff --git a/Podfile b/Podfile index 16b550efd..a9c67f1e5 100644 --- a/Podfile +++ b/Podfile @@ -130,11 +130,6 @@ abstract_target 'RiotPods' do end -plugin 'cocoapods-keys', { - :project => "Riot", - :keys => ["MapTilerAPIKey"] -} - post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Podfile.lock b/Podfile.lock index 020d05861..72b989244 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -49,7 +49,6 @@ PODS: - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - - Keys (1.0.1) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) - KTCenterFlowLayout (1.3.1) @@ -115,7 +114,6 @@ DEPENDENCIES: - HPGrowingTextView (~> 1.1) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - - Keys (from `Pods/CocoaPodsKeys`) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - MatrixSDK (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `langleyd/5292_refresh_tokens`) @@ -212,7 +210,6 @@ SPEC CHECKSUMS: Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 - Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json index 707b2f06b..0bf02ac7f 100644 --- a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json new file mode 100644 index 000000000..945c5c337 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "poll_type_checkbox_default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "poll_type_checkbox_default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "poll_type_checkbox_default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png new file mode 100644 index 000000000..84e419079 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png new file mode 100644 index 000000000..7e6083bc3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png new file mode 100644 index 000000000..316a8eab7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json new file mode 100644 index 000000000..b7fbce06b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "poll_type_checkbox_selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "poll_type_checkbox_selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "poll_type_checkbox_selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png new file mode 100644 index 000000000..6a744d6be Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png new file mode 100644 index 000000000..67c3bbd64 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png new file mode 100644 index 000000000..a4cd21452 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 32a5fc949..e28693c92 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -81,6 +81,18 @@ "accessibility_checkbox_label" = "checkbox"; "accessibility_button_label" = "button"; +// Onboarding +"onboarding_splash_register_button_title" = "Create account"; +"onboarding_splash_login_button_title" = "I already have an account"; +"onboarding_splash_page_1_title" = "Own your conversations."; +"onboarding_splash_page_1_message" = "Secure and independent communication that gives you the same level of privacy as a face-to-face conversation in your own home."; +"onboarding_splash_page_2_title" = "You’re in control."; +"onboarding_splash_page_2_message" = "Choose where your conversations are kept, giving you control and independence. Connected via Matrix."; +"onboarding_splash_page_3_title" = "Secure messaging."; +"onboarding_splash_page_3_message" = "End-to-end encrypted and no phone number required. No ads or datamining."; +"onboarding_splash_page_4_title_no_pun" = "Messaging for your team."; +"onboarding_splash_page_4_message" = "Element is also great for the workplace. It’s trusted by the world’s most secure organisations."; + // Authentication "auth_login" = "Log in"; "auth_register" = "Register"; @@ -1808,6 +1820,8 @@ Tap the + to start adding people."; "poll_edit_form_create_poll" = "Create poll"; +"poll_edit_form_poll_type" = "Poll type"; + "poll_edit_form_poll_question_or_topic" = "Poll question or topic"; "poll_edit_form_question_or_topic" = "Question or topic"; @@ -1824,6 +1838,18 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; +"poll_edit_form_update_failure_title" = "Failed to update poll"; + +"poll_edit_form_update_failure_subtitle" = "Please try again"; + +"poll_edit_form_poll_type_open" = "Open poll"; + +"poll_edit_form_poll_type_open_description" = "Voters see results as soon as they have voted"; + +"poll_edit_form_poll_type_closed" = "Closed poll"; + +"poll_edit_form_poll_type_closed_description" = "Results are only revealed when you end the poll"; + "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; diff --git a/Riot/Categories/MXKRoomBubbleCellData+Riot.swift b/Riot/Categories/MXKRoomBubbleCellData+Riot.swift new file mode 100644 index 000000000..b7e7eaa9c --- /dev/null +++ b/Riot/Categories/MXKRoomBubbleCellData+Riot.swift @@ -0,0 +1,33 @@ +// +// 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 + +extension MXKRoomBubbleCellData { + + /// Indicate true if the sender is the session user + var isSenderCurrentUser: Bool { + if let senderId = self.senderId, let currentUserId = self.mxSession.myUserId, senderId == currentUserId { + return true + } + return false + } + + // Indicate true if the cell data is collapsable and collapsed + var isCollapsableAndCollapsed: Bool { + return self.collapsable && self.collapsed + } +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index dca358737..86ed4017a 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -155,6 +155,8 @@ internal enum Asset { internal static let pollDeleteOptionIcon = ImageAsset(name: "poll_delete_option_icon") internal static let pollEditIcon = ImageAsset(name: "poll_edit_icon") internal static let pollEndIcon = ImageAsset(name: "poll_end_icon") + internal static let pollTypeCheckboxDefault = ImageAsset(name: "poll_type_checkbox_default") + internal static let pollTypeCheckboxSelected = ImageAsset(name: "poll_type_checkbox_selected") internal static let pollWinnerIcon = ImageAsset(name: "poll_winner_icon") internal static let urlPreviewClose = ImageAsset(name: "url_preview_close") internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3893e0098..bb758fd51 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2347,6 +2347,46 @@ public class VectorL10n: NSObject { public static var on: String { return VectorL10n.tr("Vector", "on") } + /// I already have an account + public static var onboardingSplashLoginButtonTitle: String { + return VectorL10n.tr("Vector", "onboarding_splash_login_button_title") + } + /// Secure and independent communication that gives you the same level of privacy as a face-to-face conversation in your own home. + public static var onboardingSplashPage1Message: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_1_message") + } + /// Own your conversations. + public static var onboardingSplashPage1Title: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_1_title") + } + /// Choose where your conversations are kept, giving you control and independence. Connected via Matrix. + public static var onboardingSplashPage2Message: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_2_message") + } + /// You’re in control. + public static var onboardingSplashPage2Title: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_2_title") + } + /// End-to-end encrypted and no phone number required. No ads or datamining. + public static var onboardingSplashPage3Message: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_3_message") + } + /// Secure messaging. + public static var onboardingSplashPage3Title: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_3_title") + } + /// Element is also great for the workplace. It’s trusted by the world’s most secure organisations. + public static var onboardingSplashPage4Message: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_4_message") + } + /// Messaging for your team. + public static var onboardingSplashPage4TitleNoPun: String { + return VectorL10n.tr("Vector", "onboarding_splash_page_4_title_no_pun") + } + /// Create account + public static var onboardingSplashRegisterButtonTitle: String { + return VectorL10n.tr("Vector", "onboarding_splash_register_button_title") + } /// Open public static var `open`: String { return VectorL10n.tr("Vector", "open") @@ -2495,6 +2535,26 @@ public class VectorL10n: NSObject { public static var pollEditFormPollQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") } + /// Poll type + public static var pollEditFormPollType: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type") + } + /// Closed poll + public static var pollEditFormPollTypeClosed: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_closed") + } + /// Results are only revealed when you end the poll + public static var pollEditFormPollTypeClosedDescription: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_closed_description") + } + /// Open poll + public static var pollEditFormPollTypeOpen: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_open") + } + /// Voters see results as soon as they have voted + public static var pollEditFormPollTypeOpenDescription: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_open_description") + } /// Please try again public static var pollEditFormPostFailureSubtitle: String { return VectorL10n.tr("Vector", "poll_edit_form_post_failure_subtitle") @@ -2508,6 +2568,14 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } /// Please try again + public static var pollEditFormUpdateFailureSubtitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_subtitle") + } + /// Failed to update poll + public static var pollEditFormUpdateFailureTitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title") + } + /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 852dedae5..f672db25d 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -184,10 +184,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - - @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) - var roomScreenAllowPollsAction - + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) var roomScreenAllowLocationAction @@ -197,6 +194,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenEnableMessageBubbles", defaultValue: BuildSettings.roomScreenEnableMessageBubblesByDefault, storage: defaults) var roomScreenEnableMessageBubbles + var roomTimelineStyleIdentifier: RoomTimelineStyleIdentifier { + return self.roomScreenEnableMessageBubbles ? .bubble : .plain + } + // MARK: - Room Contextual Menu @UserDefault(key: "roomContextualMenuShowMoreOptionForMessages", defaultValue: BuildSettings.roomContextualMenuShowMoreOptionForMessages, storage: defaults) diff --git a/Riot/Managers/Theme/ThemeService.m b/Riot/Managers/Theme/ThemeService.m index b205bbb6d..7adcadd7c 100644 --- a/Riot/Managers/Theme/ThemeService.m +++ b/Riot/Managers/Theme/ThemeService.m @@ -52,7 +52,7 @@ NSString *const kThemeServiceDidChangeThemeNotification = @"kThemeServiceDidChan [self updateAppearance]; - [[NSNotificationCenter defaultCenter] postNotificationName:kThemeServiceDidChangeThemeNotification object:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:kThemeServiceDidChangeThemeNotification object:self]; } } diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 5f823f279..3f484f317 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -482,7 +482,8 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; // Hide input view when there is only social login actions to present if ((self.authType == MXKAuthenticationTypeLogin || self.authType == MXKAuthenticationTypeRegister) && self.currentLoginSSOFlow - && !self.isAuthSessionContainsPasswordFlow) + && !self.isAuthSessionContainsPasswordFlow + && BuildSettings.authScreenShowSocialLoginSection) { hideAuthInputView = YES; } @@ -1735,8 +1736,8 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)updateSocialLoginViewVisibility { SocialLoginButtonMode socialLoginButtonMode = SocialLoginButtonModeContinue; - - BOOL showSocialLoginView = self.currentLoginSSOFlow ? YES : NO; + + BOOL showSocialLoginView = BuildSettings.authScreenShowSocialLoginSection && (self.currentLoginSSOFlow ? YES : NO); switch (self.authType) { diff --git a/Riot/Modules/Common/CollectionView/CollectionViewRightAlignFlowLayout.swift b/Riot/Modules/Common/CollectionView/CollectionViewRightAlignFlowLayout.swift new file mode 100644 index 000000000..946388de3 --- /dev/null +++ b/Riot/Modules/Common/CollectionView/CollectionViewRightAlignFlowLayout.swift @@ -0,0 +1,29 @@ +// +// 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 UIKit + +/// UICollectionViewFlowLayout with right align behavior +class CollectionViewRightAlignFlowLayout: UICollectionViewFlowLayout { + + override var flipsHorizontallyInOppositeLayoutDirection: Bool { + return true + } + + override var developmentLayoutDirection: UIUserInterfaceLayoutDirection { + return UIUserInterfaceLayoutDirection.rightToLeft + } +} diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 0b8e9efbb..f950b2b23 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -193,11 +193,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [ThemeService.shared.theme applyStyleOnSearchBar:tableSearchBar]; [ThemeService.shared.theme applyStyleOnSearchBar:self.recentsSearchBar]; - if (self.recentsTableView.dataSource) - { - // Force table refresh - [self cancelEditionMode:YES]; - } + // Force table refresh + [self.recentsTableView reloadData]; [self.emptyView updateWithTheme:ThemeService.shared.theme]; diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index 942eeec58..0028f45f3 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -36,16 +36,6 @@ #import "MXKRoomBubbleCellData.h" -#import "MXKRoomIncomingTextMsgBubbleCell.h" -#import "MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.h" -#import "MXKRoomIncomingAttachmentBubbleCell.h" -#import "MXKRoomIncomingAttachmentWithoutSenderInfoBubbleCell.h" - -#import "MXKRoomOutgoingTextMsgBubbleCell.h" -#import "MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h" -#import "MXKRoomOutgoingAttachmentBubbleCell.h" -#import "MXKRoomOutgoingAttachmentWithoutSenderInfoBubbleCell.h" - #import "MXKEncryptionKeysImportView.h" #import "NSBundle+MatrixKit.h" @@ -616,17 +606,6 @@ _bubblesTableView.delegate = self; _bubblesTableView.dataSource = roomDataSource; // Note: data source may be nil here, it will be set during [displayRoom:] call. - // Set up default classes to use for cells - [_bubblesTableView registerClass:MXKRoomIncomingTextMsgBubbleCell.class forCellReuseIdentifier:MXKRoomIncomingTextMsgBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomIncomingAttachmentBubbleCell.class forCellReuseIdentifier:MXKRoomIncomingAttachmentBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomIncomingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:MXKRoomIncomingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - - [_bubblesTableView registerClass:MXKRoomOutgoingTextMsgBubbleCell.class forCellReuseIdentifier:MXKRoomOutgoingTextMsgBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomOutgoingAttachmentBubbleCell.class forCellReuseIdentifier:MXKRoomOutgoingAttachmentBubbleCell.defaultReuseIdentifier]; - [_bubblesTableView registerClass:MXKRoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:MXKRoomOutgoingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - // Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room. MXWeakify(self); _mxSessionWillLeaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -2477,64 +2456,7 @@ - (Class)cellViewClassForCellData:(MXKCellData*)cellData { - Class cellViewClass = nil; - - // Sanity check - if ([cellData conformsToProtocol:@protocol(MXKRoomBubbleCellDataStoring)]) - { - id bubbleData = (id)cellData; - - // Select the suitable table view cell class - if (bubbleData.isIncoming) - { - if (bubbleData.isAttachmentWithThumbnail) - { - if (bubbleData.shouldHideSenderInformation) - { - cellViewClass = MXKRoomIncomingAttachmentWithoutSenderInfoBubbleCell.class; - } - else - { - cellViewClass = MXKRoomIncomingAttachmentBubbleCell.class; - } - } - else - { - if (bubbleData.shouldHideSenderInformation) - { - cellViewClass = MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.class; - } - else - { - cellViewClass = MXKRoomIncomingTextMsgBubbleCell.class; - } - } - } - else if (bubbleData.isAttachmentWithThumbnail) - { - if (bubbleData.shouldHideSenderInformation) - { - cellViewClass = MXKRoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class; - } - else - { - cellViewClass = MXKRoomOutgoingAttachmentBubbleCell.class; - } - } - else - { - if (bubbleData.shouldHideSenderInformation) - { - cellViewClass = MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class; - } - else - { - cellViewClass = MXKRoomOutgoingTextMsgBubbleCell.class; - } - } - } - - return cellViewClass; + return nil; } - (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData diff --git a/Riot/Modules/MatrixKit/MatrixKit-Bridging-Header.h b/Riot/Modules/MatrixKit/MatrixKit-Bridging-Header.h index 1653472db..3f50b38c7 100644 --- a/Riot/Modules/MatrixKit/MatrixKit-Bridging-Header.h +++ b/Riot/Modules/MatrixKit/MatrixKit-Bridging-Header.h @@ -14,3 +14,6 @@ #import "MXKRoomInputToolbarView.h" #import "MXKImageView.h" +#import "MXKRoomBubbleTableViewCell.h" +#import "MXKRoomBubbleCellData.h" +#import "MXKRoomBubbleTableViewCell.h" diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h index 691f092e6..2bd19ed7a 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h @@ -163,4 +163,11 @@ */ - (MXKRoomBubbleComponent*)getFirstBubbleComponentWithDisplay; +/** + Get the last visible component. + + @return Last visible component or nil. + */ +- (MXKRoomBubbleComponent*)getLastBubbleComponentWithDisplay; + @end diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index b20bcb668..84912efb1 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -306,16 +306,15 @@ return first; } -- (MXKRoomBubbleComponent*) getFirstBubbleComponentWithDisplay +- (MXKRoomBubbleComponent*)getFirstBubbleComponentWithDisplay { // Look for the first component which is actually displayed (some event are ignored in room history display). MXKRoomBubbleComponent* first = nil; @synchronized(bubbleComponents) { - for (NSInteger index = 0; index < bubbleComponents.count; index++) + for (MXKRoomBubbleComponent *component in bubbleComponents) { - MXKRoomBubbleComponent *component = bubbleComponents[index]; if (component.attributedTextMessage) { first = component; @@ -327,6 +326,26 @@ return first; } +- (MXKRoomBubbleComponent*)getLastBubbleComponentWithDisplay +{ + // Look for the first component which is actually displayed (some event are ignored in room history display). + MXKRoomBubbleComponent* lastVisibleComponent = nil; + + @synchronized(bubbleComponents) + { + for (MXKRoomBubbleComponent *component in bubbleComponents.reverseObjectEnumerator) + { + if (component.attributedTextMessage) + { + lastVisibleComponent = component; + break; + } + } + } + + return lastVisibleComponent; +} + - (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor { NSAttributedString *customAttributedTextMsg; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index c439bb54b..a0da54d62 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2050,8 +2050,8 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self removeEventWithEventId:eventId]; if (event.isVoiceMessage) { - NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioDuration]; - NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioWaveform]; + NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; + NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; } else { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 46abe46ab..1533da277 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1577,6 +1577,11 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } case MXEventTypePollStart: { + if (event.isEditEvent) + { + return nil; + } + displayText = [MXEventContentPollStart modelFromJSON:event.content].question; break; } diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h index 60cad3248..4f2873629 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h @@ -325,4 +325,7 @@ extern NSString *const kMXKRoomBubbleCellUrlItemInteraction; */ - (void)setupViews; +/// Add temporary subview to `tmpSubviews` property. +- (void)addTemporarySubview:(UIView*)subview; + @end diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m index b550a1ef1..b79ea8667 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m @@ -1130,6 +1130,16 @@ static BOOL _disableLongPressGestureOnEvent; [self resetAttachmentViewBottomConstraintConstant]; } +- (void)addTemporarySubview:(UIView*)subview +{ + if (!self.tmpSubviews) + { + self.tmpSubviews = [NSMutableArray new]; + } + + [self.tmpSubviews addObject:subview]; +} + #pragma mark - Attachment progress handling - (void)updateProgressUI:(NSDictionary*)statisticsDict diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift index a5dcb54ac..8248786a5 100644 --- a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift @@ -18,6 +18,13 @@ import Foundation import MatrixSDK import Reusable import DGCollectionViewLeftAlignFlowLayout +import UIKit + +/// BubbleReactionsView items alignment +enum BubbleReactionsViewAlignment { + case left + case right +} @objcMembers final class BubbleReactionsView: UIView, NibOwnerLoadable { @@ -51,6 +58,12 @@ final class BubbleReactionsView: UIView, NibOwnerLoadable { } } + var alignment: BubbleReactionsViewAlignment = .left { + didSet { + self.updateCollectionViewLayout(for: alignment) + } + } + // MARK: - Setup private func commonInit() { @@ -87,7 +100,18 @@ final class BubbleReactionsView: UIView, NibOwnerLoadable { self.collectionView.isScrollEnabled = false self.collectionView.delegate = self self.collectionView.dataSource = self - self.collectionView.collectionViewLayout = DGCollectionViewLeftAlignFlowLayout() + self.alignment = .left + + self.collectionView.register(cellType: BubbleReactionViewCell.self) + self.collectionView.register(cellType: BubbleReactionActionViewCell.self) + self.collectionView.reloadData() + } + + private func updateCollectionViewLayout(for alignment: BubbleReactionsViewAlignment) { + + let collectionViewLayout = self.collectionViewLayout(for: alignment) + + self.collectionView.collectionViewLayout = collectionViewLayout if let collectionViewFlowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout { collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize @@ -95,9 +119,22 @@ final class BubbleReactionsView: UIView, NibOwnerLoadable { collectionViewFlowLayout.minimumLineSpacing = Constants.minimumLineSpacing } - self.collectionView.register(cellType: BubbleReactionViewCell.self) - self.collectionView.register(cellType: BubbleReactionActionViewCell.self) self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + } + + private func collectionViewLayout(for alignment: BubbleReactionsViewAlignment) -> UICollectionViewLayout { + + let collectionViewLayout: UICollectionViewLayout + + switch alignment { + case .left: + collectionViewLayout = DGCollectionViewLeftAlignFlowLayout() + case .right: + collectionViewLayout = CollectionViewRightAlignFlowLayout() + } + + return collectionViewLayout } private func setupLongPressGestureRecognizer() { diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 81867be76..cd0a25f62 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -279,6 +279,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat if (self.tag == RoomBubbleCellDataTagPoll) { + if (self.events.lastObject.isEditEvent) { + return YES; + } + return NO; } @@ -826,6 +830,12 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState { + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + if (NO == [timelineConfiguration.currentStyle canAddEvent:event and:roomState to:self]) { + return NO; + } + BOOL shouldAddEvent = YES; switch (self.tag) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 1f6b51137..0a3293b91 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -321,6 +321,8 @@ const CGFloat kTypingCellHeight = 24; UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; + id cellDecorator = [RoomTimelineConfiguration shared].currentStyle.cellDecorator; + // Finalize cell view customization here if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) { @@ -332,11 +334,8 @@ const CGFloat kTypingCellHeight = 24; BOOL isCollapsableCellCollapsed = cellData.collapsable && cellData.collapsed; - // Display timestamp of the last message - if (cellData.containsLastMessage && !isCollapsableCellCollapsed) - { - [bubbleCell addTimestampLabelForComponent:cellData.mostRecentComponentIndex]; - } + // Display timestamp of the message if needed + [cellDecorator addTimestampLabelIfNeededToCell:bubbleCell cellData:cellData]; NSMutableArray *temporaryViews = [NSMutableArray new]; @@ -381,27 +380,8 @@ const CGFloat kTypingCellHeight = 24; [temporaryViews addObject:urlPreviewView]; - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray array]; - } - [bubbleCell.tmpSubviews addObject:urlPreviewView]; - - urlPreviewView.translatesAutoresizingMaskIntoConstraints = NO; - urlPreviewView.availableWidth = cellData.maxTextViewWidth; - [bubbleCell.contentView addSubview:urlPreviewView]; - - CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; - if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) - { - leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; - } - - // Set the preview view's origin - [NSLayoutConstraint activateConstraints: @[ - [urlPreviewView.leadingAnchor constraintEqualToAnchor:urlPreviewView.superview.leadingAnchor constant:leftMargin], - [urlPreviewView.topAnchor constraintEqualToAnchor:urlPreviewView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin], - ]]; + [cellDecorator addURLPreviewView:urlPreviewView + toCell:bubbleCell cellData:cellData contentViewPositionY:bottomPositionY]; } MXAggregatedReactions* reactions = cellData.reactions[componentEventId].aggregatedReactionsWithNonZeroCount; @@ -424,48 +404,8 @@ const CGFloat kTypingCellHeight = 24; [temporaryViews addObject:reactionsView]; - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray array]; - } - [bubbleCell.tmpSubviews addObject:reactionsView]; - - if ([[bubbleCell class] conformsToProtocol:@protocol(BubbleCellReactionsDisplayable)]) - { - id reactionsDisplayable = (id)bubbleCell; - [reactionsDisplayable addReactionsView:reactionsView]; - } - else - { - reactionsView.translatesAutoresizingMaskIntoConstraints = NO; - [bubbleCell.contentView addSubview:reactionsView]; - - CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; - - if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) - { - leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; - } - - // The top constraint may need to include the URL preview view - NSLayoutConstraint *topConstraint; - if (urlPreviewView) - { - topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.reactionsViewTopMargin]; - } - else - { - topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin]; - } - - // Force receipts container size - [NSLayoutConstraint activateConstraints: - @[ - [reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:leftMargin], - [reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin], - topConstraint - ]]; - } + [cellDecorator addReactionView:reactionsView toCell:bubbleCell + cellData:cellData contentViewPositionY:bottomPositionY upperDecorationView:urlPreviewView]; } MXKReceiptSendersContainer* avatarsContainer; @@ -524,70 +464,13 @@ const CGFloat kTypingCellHeight = 24; [temporaryViews addObject:avatarsContainer]; - // Add this read receipts container in the content view - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray arrayWithArray:@[avatarsContainer]]; - } - else - { - [bubbleCell.tmpSubviews addObject:avatarsContainer]; - } + UIView *upperDecorationView = reactionsView ?: urlPreviewView; - if ([[bubbleCell class] conformsToProtocol:@protocol(BubbleCellReadReceiptsDisplayable)]) - { - id readReceiptsDisplayable = (id)bubbleCell; - - [readReceiptsDisplayable addReadReceiptsView:avatarsContainer]; - } - else - { - [bubbleCell.contentView addSubview:avatarsContainer]; - - // Force receipts container size - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:RoomBubbleCellLayout.readReceiptsViewWidth]; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:RoomBubbleCellLayout.readReceiptsViewHeight]; - - // Force receipts container position - NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:avatarsContainer.superview - attribute:NSLayoutAttributeTrailing - multiplier:1.0 - constant:-RoomBubbleCellLayout.readReceiptsViewRightMargin]; - - // At the bottom, we either have reactions, a URL preview or nothing - NSLayoutConstraint *topConstraint; - if (reactionsView) - { - topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; - } - else if (urlPreviewView) - { - topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; - } - else - { - topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:avatarsContainer.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.readReceiptsViewTopMargin]; - } - - - // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, trailingConstraint]]; - } + [cellDecorator addReadReceiptsView:avatarsContainer + toCell:bubbleCell + cellData:cellData + contentViewPositionY:bottomPositionY + upperDecorationView:upperDecorationView]; } } @@ -670,16 +553,7 @@ const CGFloat kTypingCellHeight = 24; // Check whether an event is currently selected: the other messages are then blurred if (_selectedEventId) { - // Check whether the selected event belongs to this bubble - NSInteger selectedComponentIndex = cellData.selectedComponentIndex; - if (selectedComponentIndex != NSNotFound) - { - [bubbleCell selectComponent:cellData.selectedComponentIndex showEditButton:NO showTimestamp:cellData.showTimestampForSelectedComponent]; - } - else - { - bubbleCell.blurred = YES; - } + [[RoomTimelineConfiguration shared].currentStyle applySelectedStyleIfNeededToCell:bubbleCell cellData:cellData]; } // Reset the marker if any @@ -715,13 +589,24 @@ const CGFloat kTypingCellHeight = 24; // We are interested only by outgoing messages if ([cellData.senderId isEqualToString: self.mxSession.credentials.userId]) { - [bubbleCell updateTickViewWithFailedEventIds:self.failedEventIds]; + [cellDecorator addSendStatusViewToCell:bubbleCell + withFailedEventIds:self.failedEventIds]; } + + // Make extra cell layout updates if needed + [self updateCellLayoutIfNeeded:bubbleCell withCellData:cellData]; } return cell; } +- (void)updateCellLayoutIfNeeded:(MXKRoomBubbleTableViewCell*)cell withCellData:(MXKRoomBubbleCellData*)cellData { + + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + [timelineConfiguration.currentStyle.cellLayoutUpdater updateLayoutIfNeededFor:cell andCellData:cellData]; +} + - (RoomBubbleCellData*)roomBubbleCellDataForEventId:(NSString*)eventId { id cellData = [self cellDataOfEventWithEventId:eventId]; diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.swift b/Riot/Modules/Room/Location/LocationMarkerView.swift similarity index 93% rename from Riot/Modules/Room/Location/LocationUserMarkerView.swift rename to Riot/Modules/Room/Location/LocationMarkerView.swift index 995d1ae56..caf101a1b 100644 --- a/Riot/Modules/Room/Location/LocationUserMarkerView.swift +++ b/Riot/Modules/Room/Location/LocationMarkerView.swift @@ -18,7 +18,7 @@ import UIKit import Reusable import Mapbox -class LocationUserMarkerView: MGLAnnotationView, NibLoadable { +class LocationMarkerView: MGLAnnotationView, NibLoadable { @IBOutlet private var avatarView: UserAvatarView! diff --git a/Riot/Modules/Room/Location/LocationMarkerView.xib b/Riot/Modules/Room/Location/LocationMarkerView.xib new file mode 100644 index 000000000..837db5503 --- /dev/null +++ b/Riot/Modules/Room/Location/LocationMarkerView.xib @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.xib b/Riot/Modules/Room/Location/LocationUserMarkerView.xib deleted file mode 100644 index 26495f925..000000000 --- a/Riot/Modules/Room/Location/LocationUserMarkerView.xib +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 82cebe7cd..17ee83b4c 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -17,7 +17,6 @@ import UIKit import Reusable import Mapbox -import Keys class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { @@ -25,7 +24,6 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat private struct Constants { static let mapHeight: CGFloat = 300.0 - static let mapTilerKey = RiotKeys().mapTilerAPIKey static let mapZoomLevel = 15.0 static let cellBorderRadius: CGFloat = 1.0 static let cellCornerRadius: CGFloat = 8.0 @@ -36,9 +34,10 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat @IBOutlet private var descriptionContainerView: UIView! @IBOutlet private var descriptionLabel: UILabel! + @IBOutlet private var descriptionIcon: UIImageView! private var mapView: MGLMapView! - private var annotationView: LocationUserMarkerView? + private var annotationView: LocationMarkerView? // MARK: Public @@ -73,19 +72,13 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat // MARK: - Public - public func displayLocation(_ location: CLLocationCoordinate2D, - userIdentifier: String, - userDisplayName: String, - userAvatarURLString: String?, - mediaManager: MXMediaManager) { - - annotationView = LocationUserMarkerView.loadFromNib() + public func displayLocation(_ location: CLLocationCoordinate2D, userAvatarData: AvatarViewData? = nil) { - annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, - displayName: userDisplayName, - avatarUrl: userAvatarURLString, - mediaManager: mediaManager, - fallbackImage: .matrixItem(userIdentifier, userDisplayName))) + annotationView = LocationMarkerView.loadFromNib() + + if let userAvatarData = userAvatarData { + annotationView?.setAvatarData(userAvatarData) + } if let annotations = mapView.annotations { mapView.removeAnnotations(annotations) @@ -103,6 +96,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat func update(theme: Theme) { descriptionLabel.textColor = theme.colors.primaryContent descriptionLabel.font = theme.fonts.footnote + descriptionIcon.tintColor = theme.colors.accent layer.borderColor = theme.colors.quinaryContent.cgColor } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index 8beacacbe..acc469b74 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -58,6 +58,7 @@ + diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index fec16dd93..ef5116b17 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -76,7 +76,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.activityIndicatorPresenter = ActivityIndicatorPresenter() if #available(iOS 14, *) { - PollTimelineProvider.shared.session = parameters.session + TimelinePollProvider.shared.session = parameters.session } super.init() @@ -246,6 +246,29 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter.present(coordinator, animated: true) coordinator.start() } + + private func startEditPollCoordinator(startEvent: MXEvent? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room, pollStartEvent: startEvent) + let coordinator = PollEditFormCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter?.present(coordinator, animated: true) + coordinator.start() + } } // MARK: - RoomIdentifiable @@ -305,26 +328,7 @@ extension RoomCoordinator: RoomViewControllerDelegate { } func roomViewControllerDidRequestPollCreationFormPresentation(_ roomViewController: RoomViewController) { - guard #available(iOS 14.0, *) else { - return - } - - let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) - let coordinator = PollEditFormCoordinator(parameters: parameters) - - coordinator.completion = { [weak self, weak coordinator] in - guard let self = self, let coordinator = coordinator else { - return - } - - self.navigationRouter?.dismissModule(animated: true, completion: nil) - self.remove(childCoordinator: coordinator) - } - - add(childCoordinator: coordinator) - - navigationRouter?.present(coordinator, animated: true) - coordinator.start() + startEditPollCoordinator() } func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { @@ -340,7 +344,7 @@ extension RoomCoordinator: RoomViewControllerDelegate { return false } - return PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.canEndPoll() ?? false + return TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.canEndPoll() ?? false } func roomViewController(_ roomViewController: RoomViewController, endPollWithEventIdentifier eventIdentifier: String) { @@ -348,6 +352,18 @@ extension RoomCoordinator: RoomViewControllerDelegate { return } - PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() + TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() + } + + func roomViewController(_ roomViewController: RoomViewController, canEditPollWithEventIdentifier eventIdentifier: String) -> Bool { + guard #available(iOS 14.0, *) else { + return false + } + + return TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.canEditPoll() ?? false + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestEditForPollWithStart startEvent: MXEvent) { + startEditPollCoordinator(startEvent: startEvent) } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index f3b9f6fa6..6e30853af 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -207,6 +207,12 @@ canEndPollWithEventIdentifier:(NSString *)eventIdentifier; - (void)roomViewController:(RoomViewController *)roomViewController endPollWithEventIdentifier:(NSString *)eventIdentifier; +- (BOOL)roomViewController:(RoomViewController *)roomViewController +canEditPollWithEventIdentifier:(NSString *)eventIdentifier; + +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 10af1f3bc..35117f3be 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -54,55 +54,7 @@ #import "JitsiViewController.h" #import "RoomEmptyBubbleCell.h" - -#import "RoomIncomingTextMsgBubbleCell.h" -#import "RoomIncomingTextMsgWithoutSenderInfoBubbleCell.h" -#import "RoomIncomingTextMsgWithPaginationTitleBubbleCell.h" -#import "RoomIncomingTextMsgWithoutSenderNameBubbleCell.h" -#import "RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" -#import "RoomIncomingAttachmentBubbleCell.h" -#import "RoomIncomingAttachmentWithoutSenderInfoBubbleCell.h" -#import "RoomIncomingAttachmentWithPaginationTitleBubbleCell.h" - -#import "RoomIncomingEncryptedTextMsgBubbleCell.h" -#import "RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.h" -#import "RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.h" -#import "RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.h" -#import "RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" -#import "RoomIncomingEncryptedAttachmentBubbleCell.h" -#import "RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.h" -#import "RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.h" - -#import "RoomOutgoingTextMsgBubbleCell.h" -#import "RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h" -#import "RoomOutgoingTextMsgWithPaginationTitleBubbleCell.h" -#import "RoomOutgoingTextMsgWithoutSenderNameBubbleCell.h" -#import "RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" -#import "RoomOutgoingAttachmentBubbleCell.h" -#import "RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.h" -#import "RoomOutgoingAttachmentWithPaginationTitleBubbleCell.h" - -#import "RoomOutgoingEncryptedTextMsgBubbleCell.h" -#import "RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.h" -#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.h" -#import "RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.h" -#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" -#import "RoomOutgoingEncryptedAttachmentBubbleCell.h" -#import "RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.h" -#import "RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.h" - -#import "RoomMembershipBubbleCell.h" -#import "RoomMembershipWithPaginationTitleBubbleCell.h" -#import "RoomMembershipCollapsedBubbleCell.h" -#import "RoomMembershipCollapsedWithPaginationTitleBubbleCell.h" #import "RoomMembershipExpandedBubbleCell.h" -#import "RoomMembershipExpandedWithPaginationTitleBubbleCell.h" -#import "RoomCreationWithPaginationCollapsedBubbleCell.h" -#import "RoomCreationCollapsedBubbleCell.h" - -#import "RoomSelectedStickerBubbleCell.h" -#import "RoomPredecessorBubbleCell.h" - #import "MXKRoomBubbleTableViewCell+Riot.h" #import "AvatarGenerator.h" @@ -130,6 +82,8 @@ #import "MXSDKOptions.h" +#import "RoomTimelineCellProvider.h" + #import "GeneratedInterface-Swift.h" NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; @@ -349,83 +303,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [super viewDidLoad]; // Register first customized cell view classes used to render bubbles - [self.bubblesTableView registerClass:RoomIncomingTextMsgBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingAttachmentBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomIncomingEncryptedTextMsgBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedAttachmentBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomOutgoingAttachmentBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingTextMsgBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomOutgoingEncryptedAttachmentBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomMembershipBubbleCell.class forCellReuseIdentifier:RoomMembershipBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomMembershipWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomMembershipCollapsedBubbleCell.class forCellReuseIdentifier:RoomMembershipCollapsedBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomMembershipCollapsedWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipCollapsedWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomMembershipExpandedBubbleCell.class forCellReuseIdentifier:RoomMembershipExpandedBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomMembershipExpandedWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipExpandedWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomSelectedStickerBubbleCell.class forCellReuseIdentifier:RoomSelectedStickerBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomPredecessorBubbleCell.class forCellReuseIdentifier:RoomPredecessorBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:KeyVerificationIncomingRequestApprovalBubbleCell.class forCellReuseIdentifier:KeyVerificationIncomingRequestApprovalBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:KeyVerificationRequestStatusBubbleCell.class forCellReuseIdentifier:KeyVerificationRequestStatusBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:KeyVerificationRequestStatusWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationRequestStatusWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:KeyVerificationConclusionBubbleCell.class forCellReuseIdentifier:KeyVerificationConclusionBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:KeyVerificationConclusionWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationConclusionWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomCreationCollapsedBubbleCell.class forCellReuseIdentifier:RoomCreationCollapsedBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomCreationWithPaginationCollapsedBubbleCell.class forCellReuseIdentifier:RoomCreationWithPaginationCollapsedBubbleCell.defaultReuseIdentifier]; - - // call cells - [self.bubblesTableView registerClass:RoomDirectCallStatusBubbleCell.class forCellReuseIdentifier:RoomDirectCallStatusBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:RoomGroupCallStatusBubbleCell.class forCellReuseIdentifier:RoomGroupCallStatusBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:RoomCreationIntroCell.class forCellReuseIdentifier:RoomCreationIntroCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerNib:RoomTypingBubbleCell.nib forCellReuseIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:VoiceMessageBubbleCell.class forCellReuseIdentifier:VoiceMessageBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:VoiceMessageWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:VoiceMessageWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:PollBubbleCell.class forCellReuseIdentifier:PollBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:PollWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:PollWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:PollWithPaginationTitleBubbleCell.class forCellReuseIdentifier:PollWithPaginationTitleBubbleCell.defaultReuseIdentifier]; - - [self.bubblesTableView registerClass:LocationBubbleCell.class forCellReuseIdentifier:LocationBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:LocationWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [self.bubblesTableView registerClass:LocationWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [[RoomTimelineConfiguration shared].currentStyle.cellProvider registerCellsForTableView:self.bubblesTableView]; [self vc_removeBackTitle]; @@ -2026,7 +1904,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self roomInputToolbarViewDidTapFileUpload]; }]]; } - if (RiotSettings.shared.roomScreenAllowPollsAction) + if (BuildSettings.pollsEnabled) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_poll"] andAction:^{ MXStrongifyAndReturnIfNil(self); @@ -2172,6 +2050,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; [roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset]; }]; + compressionPrompt.popoverPresentationController.sourceView = roomInputToolbarView.attachMediaButton; + compressionPrompt.popoverPresentationController.sourceRect = roomInputToolbarView.attachMediaButton.bounds; [self presentViewController:compressionPrompt animated:YES completion:nil]; } @@ -2634,14 +2514,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (Class)cellViewClassForCellData:(MXKCellData*)cellData { - Class cellViewClass = nil; - BOOL showEncryptionBadge = NO; + RoomTimelineCellIdentifier cellIdentifier = [self cellIdentifierForCellData:cellData andRoomDataSource:customizedRoomDataSource]; + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + return [timelineConfiguration.currentStyle.cellProvider cellViewClassForCellIdentifier:cellIdentifier];; +} + +- (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData andRoomDataSource:(RoomDataSource *)customizedRoomDataSource; +{ // Sanity check if (![cellData conformsToProtocol:@protocol(MXKRoomBubbleCellDataStoring)]) { - return nil; + return RoomTimelineCellIdentifierUnknown; } + + BOOL showEncryptionBadge = NO; + RoomTimelineCellIdentifier cellIdentifier; id bubbleData = (id)cellData; @@ -2656,27 +2545,27 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Select the suitable table view cell class, by considering first the empty bubble cell. if (bubbleData.hasNoDisplay) { - cellViewClass = RoomEmptyBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierEmpty; } else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreationIntro) { - cellViewClass = RoomCreationIntroCell.class; + cellIdentifier = RoomTimelineCellIdentifierRoomCreationIntro; } else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor) { - cellViewClass = RoomPredecessorBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierRoomPredecessor; } else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval) { - cellViewClass = bubbleData.isPaginationFirstBubble ? KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.class : KeyVerificationIncomingRequestApprovalBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationIncomingRequestApprovalWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationIncomingRequestApproval; } else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequest) { - cellViewClass = bubbleData.isPaginationFirstBubble ? KeyVerificationRequestStatusWithPaginationTitleBubbleCell.class : KeyVerificationRequestStatusBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationRequestStatusWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationRequestStatus; } else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationConclusion) { - cellViewClass = bubbleData.isPaginationFirstBubble ? KeyVerificationConclusionWithPaginationTitleBubbleCell.class : KeyVerificationConclusionBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationConclusionWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationConclusion; } else if (bubbleData.tag == RoomBubbleCellDataTagMembership) { @@ -2684,80 +2573,80 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (bubbleData.nextCollapsableCellData) { - cellViewClass = bubbleData.isPaginationFirstBubble ? RoomMembershipCollapsedWithPaginationTitleBubbleCell.class : RoomMembershipCollapsedBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipCollapsedWithPaginationTitle : RoomTimelineCellIdentifierMembershipCollapsed; } else { // Use a normal membership cell for a single membership event - cellViewClass = bubbleData.isPaginationFirstBubble ? RoomMembershipWithPaginationTitleBubbleCell.class : RoomMembershipBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership; } } else if (bubbleData.collapsedAttributedTextMessage) { // The cell (and its series) is not collapsed but this cell is the first // of the series. So, use the cell with the "collapse" button. - cellViewClass = bubbleData.isPaginationFirstBubble ? RoomMembershipExpandedWithPaginationTitleBubbleCell.class : RoomMembershipExpandedBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipExpandedWithPaginationTitle : RoomTimelineCellIdentifierMembershipExpanded; } else { - cellViewClass = bubbleData.isPaginationFirstBubble ? RoomMembershipWithPaginationTitleBubbleCell.class : RoomMembershipBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership; } } else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateConfiguration) { - cellViewClass = bubbleData.isPaginationFirstBubble ? RoomCreationWithPaginationCollapsedBubbleCell.class : RoomCreationCollapsedBubbleCell.class; + cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierRoomCreationCollapsedWithPaginationTitle : RoomTimelineCellIdentifierRoomCreationCollapsed; } else if (bubbleData.tag == RoomBubbleCellDataTagCall) { - cellViewClass = RoomDirectCallStatusBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierDirectCallStatus; } else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall) { - cellViewClass = RoomGroupCallStatusBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierGroupCallStatus; } else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage || bubbleData.attachment.type == MXKAttachmentTypeAudio) { if (bubbleData.isPaginationFirstBubble) { - cellViewClass = VoiceMessageWithPaginationTitleBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = VoiceMessageWithoutSenderInfoBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo; } else { - cellViewClass = VoiceMessageBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierVoiceMessage; } } else if (bubbleData.tag == RoomBubbleCellDataTagPoll) { if (bubbleData.isPaginationFirstBubble) { - cellViewClass = PollWithPaginationTitleBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierPollWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = PollWithoutSenderInfoBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierPollWithoutSenderInfo; } else { - cellViewClass = PollBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierPoll; } } else if (bubbleData.tag == RoomBubbleCellDataTagLocation) { if (bubbleData.isPaginationFirstBubble) { - cellViewClass = LocationWithPaginationTitleBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierLocationWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = LocationWithoutSenderInfoBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierLocationWithoutSenderInfo; } else { - cellViewClass = LocationBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierLocation; } } else if (bubbleData.isIncoming) @@ -2767,19 +2656,19 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Check whether the provided celldata corresponds to a selected sticker if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId]) { - cellViewClass = RoomSelectedStickerBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierSelectedSticker; } else if (bubbleData.isPaginationFirstBubble) { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.class : RoomIncomingAttachmentWithPaginationTitleBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.class : RoomIncomingAttachmentWithoutSenderInfoBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutSenderInfo; } else { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedAttachmentBubbleCell.class : RoomIncomingAttachmentBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncrypted : RoomTimelineCellIdentifierIncomingAttachment; } } else @@ -2788,24 +2677,24 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (bubbleData.shouldHideSenderName) { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class : RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitleWithoutSenderName; } else { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class : RoomIncomingTextMsgWithPaginationTitleBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitle; } } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class : RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderInfo; } else if (bubbleData.shouldHideSenderName) { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.class : RoomIncomingTextMsgWithoutSenderNameBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderName; } else { - cellViewClass = showEncryptionBadge ? RoomIncomingEncryptedTextMsgBubbleCell.class : RoomIncomingTextMsgBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncrypted : RoomTimelineCellIdentifierIncomingTextMessage; } } } @@ -2817,19 +2706,19 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Check whether the provided celldata corresponds to a selected sticker if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId]) { - cellViewClass = RoomSelectedStickerBubbleCell.class; + cellIdentifier = RoomTimelineCellIdentifierSelectedSticker; } else if (bubbleData.isPaginationFirstBubble) { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.class :RoomOutgoingAttachmentWithPaginationTitleBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.class : RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutSenderInfo; } else { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedAttachmentBubbleCell.class : RoomOutgoingAttachmentBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncrypted : RoomTimelineCellIdentifierOutgoingAttachment; } } else @@ -2838,29 +2727,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (bubbleData.shouldHideSenderName) { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class : RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName; } else { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class : RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle; } } else if (bubbleData.shouldHideSenderInformation) { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class :RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo; } else if (bubbleData.shouldHideSenderName) { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class : RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName; } else { - cellViewClass = showEncryptionBadge ? RoomOutgoingEncryptedTextMsgBubbleCell.class : RoomOutgoingTextMsgBubbleCell.class; + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncrypted : RoomTimelineCellIdentifierOutgoingTextMessage; } } } - return cellViewClass; + return cellIdentifier; } #pragma mark - MXKDataSource delegate @@ -6103,16 +5992,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXWeakify(self); RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit]; - editMenuItem.action = ^{ - MXStrongifyAndReturnIfNil(self); - [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; - [self editEventContentWithId:event.eventId]; - - // And display the keyboard - [self.inputToolbarView becomeFirstResponder]; - }; - editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + switch (event.eventType) { + case MXEventTypePollStart: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:nil]; + [self.delegate roomViewController:self didRequestEditForPollWithStartEvent:event]; + }; + + editMenuItem.isEnabled = [self.delegate roomViewController:self canEditPollWithEventIdentifier:event.eventId]; + + break; + } + default: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self editEventContentWithId:event.eventId]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + }; + + editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + + break; + } + } return editMenuItem; } diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift index 2795d2647..cb56e7839 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -37,11 +37,17 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) - locationView.displayLocation(location, - userIdentifier: bubbleData.senderId, - userDisplayName: bubbleData.senderDisplayName, - userAvatarURLString: bubbleData.senderAvatarUrl, - mediaManager: bubbleData.mxSession.mediaManager) + if locationContent.assetType == .user { + let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName, + avatarUrl: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager, + fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) + + locationView.displayLocation(location, userAvatarData: avatarViewData) + } else { + locationView.displayLocation(location) + } } override func setupViews() { diff --git a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift index 4d3d42e28..2165549d8 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift @@ -29,7 +29,7 @@ class PollBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { let bubbleData = cellData as? RoomBubbleCellData, let event = bubbleData.events.last, event.eventType == __MXEventType.pollStart, - let view = PollTimelineProvider.shared.buildPollTimelineViewForEvent(event) else { + let view = TimelinePollProvider.shared.buildTimelinePollViewForEvent(event) else { return } diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/RoomBubbleCellLayout.swift similarity index 100% rename from Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift rename to Riot/Modules/Room/Views/BubbleCells/RoomBubbleCellLayout.swift diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h new file mode 100644 index 000000000..35a3b500a --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h @@ -0,0 +1,115 @@ +// +// 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. +// + +/// RoomTimelineCellIdentifier represents room timeline cell identifiers. +typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) { + + RoomTimelineCellIdentifierUnknown, + + // - Text message + // -- Incoming + // --- Clear + RoomTimelineCellIdentifierIncomingTextMessage, + RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitle, + RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderName, + RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitleWithoutSenderName, + // --- Encrypted + RoomTimelineCellIdentifierIncomingTextMessageEncrypted, + RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitle, + RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderName, + RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitleWithoutSenderName, + // -- Outgoing + // --- Clear + RoomTimelineCellIdentifierOutgoingTextMessage, + RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle, + RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName, + RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName, + // --- Encrypted + RoomTimelineCellIdentifierOutgoingTextMessageEncrypted, + RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle, + RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName, + RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName, + + // - Attachment + // -- Incoming + // --- Clear + RoomTimelineCellIdentifierIncomingAttachment, + RoomTimelineCellIdentifierIncomingAttachmentWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingAttachmentWithPaginationTitle, + // --- Encrypted + RoomTimelineCellIdentifierIncomingAttachmentEncrypted, + RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithPaginationTitle, + // -- Outgoing + // --- Clear + RoomTimelineCellIdentifierOutgoingAttachment, + RoomTimelineCellIdentifierOutgoingAttachmentWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingAttachmentWithPaginationTitle, + // --- Encrypted + RoomTimelineCellIdentifierOutgoingAttachmentEncrypted, + RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle, + + // - Room membership + RoomTimelineCellIdentifierMembership, + RoomTimelineCellIdentifierMembershipWithPaginationTitle, + RoomTimelineCellIdentifierMembershipCollapsed, + RoomTimelineCellIdentifierMembershipCollapsedWithPaginationTitle, + RoomTimelineCellIdentifierMembershipExpanded, + RoomTimelineCellIdentifierMembershipExpandedWithPaginationTitle, + + // - Key verification + RoomTimelineCellIdentifierKeyVerificationIncomingRequestApproval, + RoomTimelineCellIdentifierKeyVerificationIncomingRequestApprovalWithPaginationTitle, + RoomTimelineCellIdentifierKeyVerificationRequestStatus, + RoomTimelineCellIdentifierKeyVerificationRequestStatusWithPaginationTitle, + RoomTimelineCellIdentifierKeyVerificationConclusion, + RoomTimelineCellIdentifierKeyVerificationConclusionWithPaginationTitle, + + // - Room creation + RoomTimelineCellIdentifierRoomCreationCollapsed, + RoomTimelineCellIdentifierRoomCreationCollapsedWithPaginationTitle, + + // - Call + RoomTimelineCellIdentifierDirectCallStatus, + RoomTimelineCellIdentifierGroupCallStatus, + + // - Voice message + RoomTimelineCellIdentifierVoiceMessage, + RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo, + RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle, + + // - Poll + RoomTimelineCellIdentifierPoll, + RoomTimelineCellIdentifierPollWithoutSenderInfo, + RoomTimelineCellIdentifierPollWithPaginationTitle, + + // - Location sharing + RoomTimelineCellIdentifierLocation, + RoomTimelineCellIdentifierLocationWithoutSenderInfo, + RoomTimelineCellIdentifierLocationWithPaginationTitle, + + // - Others + RoomTimelineCellIdentifierEmpty, + RoomTimelineCellIdentifierSelectedSticker, + RoomTimelineCellIdentifierRoomPredecessor, + RoomTimelineCellIdentifierRoomCreationIntro, + RoomTimelineCellIdentifierTyping +}; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTimelineConfiguration.swift b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineConfiguration.swift new file mode 100644 index 000000000..1cd3b60d7 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineConfiguration.swift @@ -0,0 +1,96 @@ +// +// 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 + +/// RoomTimelineConfiguration enables to manage room timeline appearance configuration +@objcMembers +class RoomTimelineConfiguration: NSObject { + + // MARK: - Constants + + static let shared = RoomTimelineConfiguration() + + // MARK: - Properties + + private(set) var currentStyle: RoomTimelineStyle + + // MARK: - Setup + + init(style: RoomTimelineStyle) { + self.currentStyle = style + + super.init() + + self.registerThemeDidChange() + } + + convenience init(styleIdentifier: RoomTimelineStyleIdentifier) { + + let style = type(of: self).style(for: styleIdentifier) + self.init(style: style) + } + + convenience override init() { + let styleIdentifier = RiotSettings.shared.roomTimelineStyleIdentifier + self.init(styleIdentifier: styleIdentifier) + } + + // MARK: - Public + + func updateStyle(_ roomTimelineStyle: RoomTimelineStyle) { + self.currentStyle = roomTimelineStyle + } + + func updateStyle(withIdentifier identifier: RoomTimelineStyleIdentifier) { + + let style = type(of: self).style(for: identifier) + + self.updateStyle(style) + } + + // MARK: - Private + + private func registerThemeDidChange() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange(notification:)), name: .themeServiceDidChangeTheme, object: nil) + + } + + @objc private func themeDidChange(notification: Notification) { + + guard let themeService = notification.object as? ThemeService else { + return + } + + self.currentStyle.update(theme: themeService.theme) + } + + private class func style(for identifier: RoomTimelineStyleIdentifier) -> RoomTimelineStyle { + + let roomTimelineStyle: RoomTimelineStyle + + let theme = ThemeService.shared().theme + + switch identifier { + case .plain: + roomTimelineStyle = PlainRoomTimelineStyle(theme: theme) + case .bubble: + roomTimelineStyle = BubbleRoomTimelineStyle(theme: theme) + } + + return roomTimelineStyle + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift new file mode 100644 index 000000000..28fdcb163 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift @@ -0,0 +1,313 @@ +// +// 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 UIKit + +@objcMembers +class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { + + // MARK: - Properties + + private var theme: Theme + + private var incomingColor: UIColor { + return self.theme.colors.system + } + + private var outgoingColor: UIColor { + return self.theme.colors.accent.withAlphaComponent(0.10) + } + + // MARK: - Setup + + init(theme: Theme) { + self.theme = theme + } + + // MARK: - Public + + func updateLayoutIfNeeded(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + if cellData.isSenderCurrentUser { + self.updateLayout(forOutgoingTextMessageCell: cell, andCellData: cellData) + } else { + self.updateLayout(forIncomingTextMessageCell: cell, andCellData: cellData) + } + } + + func updateLayout(forIncomingTextMessageCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + if let messageBubbleBackgroundView = cell.messageBubbleBackgroundView { + + if self.canUseBubbleBackground(forCell: cell, withCellData: cellData) { + + messageBubbleBackgroundView.isHidden = false + + self.updateMessageBubbleBackgroundView(messageBubbleBackgroundView, withCell: cell, andCellData: cellData) + } else { + messageBubbleBackgroundView.isHidden = true + } + } + } + + func updateLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { + + if let messageBubbleBackgroundView = cell.messageBubbleBackgroundView { + + if self.canUseBubbleBackground(forCell: cell, withCellData: cellData) { + + messageBubbleBackgroundView.isHidden = false + + self.updateMessageBubbleBackgroundView(messageBubbleBackgroundView, withCell: cell, andCellData: cellData) + } else { + messageBubbleBackgroundView.isHidden = true + } + } + } + + func setupLayout(forIncomingTextMessageCell cell: MXKRoomBubbleTableViewCell) { + + self.setupIncomingMessageTextViewMargins(for: cell) + + self.addBubbleBackgroundViewToCell(cell, backgroundColor: self.incomingColor) + + cell.setNeedsUpdateConstraints() + } + + func setupLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell) { + + self.setupOutgoingMessageTextViewMargins(for: cell) + + // Hide avatar view + cell.pictureView?.isHidden = true + + self.addBubbleBackgroundViewToCell(cell, backgroundColor: self.outgoingColor) + + cell.setNeedsUpdateConstraints() + } + + // MARK: Themable + + func update(theme: Theme) { + self.theme = theme + } + + // MARK: - Private + + // MARK: Bubble background view + + private func createBubbleBackgroundView(with backgroundColor: UIColor) -> RoomMessageBubbleBackgroundView { + + let bubbleBackgroundView = RoomMessageBubbleBackgroundView() + bubbleBackgroundView.backgroundColor = backgroundColor + + return bubbleBackgroundView + } + + private func addBubbleBackgroundViewToCell(_ bubbleCell: MXKRoomBubbleTableViewCell, backgroundColor: UIColor) { + + guard let messageTextView = bubbleCell.messageTextView else { + return + } + + let topMargin: CGFloat = 0.0 + let leftMargin: CGFloat = 5.0 + let rightMargin: CGFloat = 45.0 // Add extra space for timestamp + + let bubbleBackgroundView = self.createBubbleBackgroundView(with: backgroundColor) + + bubbleCell.contentView.insertSubview(bubbleBackgroundView, at: 0) + + let topAnchor = messageTextView.topAnchor + let leadingAnchor = messageTextView.leadingAnchor + let trailingAnchor = messageTextView.trailingAnchor + + bubbleBackgroundView.updateHeight(messageTextView.frame.height) + + NSLayoutConstraint.activate([ + bubbleBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: topMargin), + bubbleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -leftMargin), + bubbleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightMargin) + ]) + } + + private func canUseBubbleBackground(forCell cell: MXKRoomBubbleTableViewCell, withCellData cellData: MXKRoomBubbleCellData) -> Bool { + + guard let firstComponent = cellData.getFirstBubbleComponentWithDisplay(), let firstEvent = firstComponent.event else { + return false + } + + switch firstEvent.eventType { + case .roomMessage: + if let messageTypeString = firstEvent.content["msgtype"] as? String { + + let messageType = MXMessageType(identifier: messageTypeString) + + switch messageType { + case .text : + return true + default: + break + } + } + default: + break + } + + return false + } + + private func getTextMessageHeight(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> CGFloat? { + + guard let roomBubbleCellData = cellData as? RoomBubbleCellData, + let lastBubbleComponent = cellData.getLastBubbleComponentWithDisplay(), + let firstComponent = roomBubbleCellData.getFirstBubbleComponentWithDisplay() else { + return nil + } + + let bubbleHeight: CGFloat + + let lastEventId = lastBubbleComponent.event.eventId + let lastMessageBottomPosition = cell.bottomPosition(ofEvent: lastEventId) + + let firstEventId = firstComponent.event.eventId + let firstMessageTopPosition = cell.topPosition(ofEvent: firstEventId) + + let additionalContentHeight = roomBubbleCellData.additionalContentHeight + + bubbleHeight = lastMessageBottomPosition - firstMessageTopPosition - additionalContentHeight + + guard bubbleHeight >= 0 else { + return nil + } + + return bubbleHeight + } + + // TODO: Improve text message height calculation + // This method is closer to final result but lack of stability because of extra vertical space not handled here. +// private func getTextMessageHeight(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> CGFloat? { +// +// guard let roomBubbleCellData = cellData as? RoomBubbleCellData, +// let firstComponent = roomBubbleCellData.getFirstBubbleComponentWithDisplay() else { +// return nil +// } +// +// let bubbleHeight: CGFloat +// +// let componentIndex = cellData.bubbleComponentIndex(forEventId: firstComponent.event.eventId) +// +// let componentFrame = cell.componentFrameInContentView(for: componentIndex) +// +// bubbleHeight = componentFrame.height +// +// guard bubbleHeight >= 0 else { +// return nil +// } +// +// return bubbleHeight +// } + + private func getMessageBubbleBackgroundHeight(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> CGFloat? { + + var finalBubbleHeight: CGFloat? + let extraMargin: CGFloat = 4.0 + + if let bubbleHeight = self.getTextMessageHeight(for: cell, andCellData: cellData) { + finalBubbleHeight = bubbleHeight + extraMargin + + } else if let messageTextViewHeight = cell.messageTextView?.frame.height { + + finalBubbleHeight = messageTextViewHeight + extraMargin + } + + return finalBubbleHeight + } + + @discardableResult + private func updateMessageBubbleBackgroundView(_ roomMessageBubbleBackgroundView: RoomMessageBubbleBackgroundView, withCell cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) -> Bool { + + if let bubbleHeight = self.getMessageBubbleBackgroundHeight(for: cell, andCellData: cellData) { + return roomMessageBubbleBackgroundView.updateHeight(bubbleHeight) + } else { + return false + } + } + + private func getIncomingMessageTextViewInsets(from bubbleCell: MXKRoomBubbleTableViewCell) -> UIEdgeInsets { + + let messageViewMarginTop: CGFloat + let messageViewMarginBottom: CGFloat = -2.0 + let messageViewMarginLeft: CGFloat = 3.0 + let messageViewMarginRight: CGFloat = 80 + + if bubbleCell.userNameLabel != nil { + messageViewMarginTop = 10.0 + } else { + messageViewMarginTop = 0.0 + } + + let messageViewInsets = UIEdgeInsets(top: messageViewMarginTop, left: messageViewMarginLeft, bottom: messageViewMarginBottom, right: messageViewMarginRight) + + return messageViewInsets + } + + // MARK: Text message + + private func setupIncomingMessageTextViewMargins(for cell: MXKRoomBubbleTableViewCell) { + + guard cell.messageTextView != nil else { + return + } + + let messageViewInsets = self.getIncomingMessageTextViewInsets(from: cell) + + cell.msgTextViewBottomConstraint.constant += messageViewInsets.bottom + cell.msgTextViewTopConstraint.constant += messageViewInsets.top + cell.msgTextViewLeadingConstraint.constant += messageViewInsets.left + cell.msgTextViewTrailingConstraint.constant += messageViewInsets.right + } + + private func setupOutgoingMessageTextViewMargins(for cell: MXKRoomBubbleTableViewCell) { + + guard let messageTextView = cell.messageTextView else { + return + } + + let contentView = cell.contentView + + let leftMargin: CGFloat = 80.0 + let rightMargin: CGFloat = 78.0 + let bottomMargin: CGFloat = -2.0 + + cell.msgTextViewLeadingConstraint.isActive = false + cell.msgTextViewTrailingConstraint.isActive = false + + let leftConstraint = messageTextView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: leftMargin) + + let rightConstraint = messageTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -rightMargin) + + NSLayoutConstraint.activate([ + leftConstraint, + rightConstraint + ]) + + cell.msgTextViewLeadingConstraint = leftConstraint + cell.msgTextViewTrailingConstraint = rightConstraint + + cell.msgTextViewBottomConstraint.constant += bottomMargin + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift new file mode 100644 index 000000000..8fd1ccbcd --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift @@ -0,0 +1,230 @@ +// +// 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 UIKit + +class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { + + override func addTimestampLabelIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + + guard self.canShowTimestamp(forCellData: cellData) else { + return + } + + self.addTimestampLabel(toCell: cell, cellData: cellData) + } + + override func addTimestampLabel(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + + // If cell contains a bubble background, add the timestamp inside of it + if let bubbleBackgroundView = cell.messageBubbleBackgroundView, bubbleBackgroundView.isHidden == false { + + let componentIndex = cellData.mostRecentComponentIndex + + guard let bubbleComponents = cellData.bubbleComponents, + componentIndex < bubbleComponents.count else { + return + } + + let component = bubbleComponents[componentIndex] + + let timestampLabel = self.createTimestampLabel(cellData: cellData, + bubbleComponent: component, + viewTag: componentIndex) + timestampLabel.translatesAutoresizingMaskIntoConstraints = false + + cell.addTemporarySubview(timestampLabel) + + bubbleBackgroundView.addSubview(timestampLabel) + + let rightMargin: CGFloat = 8.0 + let bottomMargin: CGFloat = 4.0 + + let trailingConstraint = timestampLabel.trailingAnchor.constraint(equalTo: bubbleBackgroundView.trailingAnchor, constant: -rightMargin) + + let bottomConstraint = timestampLabel.bottomAnchor.constraint(equalTo: bubbleBackgroundView.bottomAnchor, constant: -bottomMargin) + + NSLayoutConstraint.activate([ + trailingConstraint, + bottomConstraint + ]) + } else { + super.addTimestampLabel(toCell: cell, cellData: cellData) + } + } + + override func addReactionView(_ reactionsView: BubbleReactionsView, + toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData, contentViewPositionY: CGFloat, upperDecorationView: UIView?) { + + cell.addTemporarySubview(reactionsView) + + if let reactionsDisplayable = cell as? BubbleCellReactionsDisplayable { + reactionsDisplayable.addReactionsView(reactionsView) + return + } + + reactionsView.translatesAutoresizingMaskIntoConstraints = false + + let cellContentView = cell.contentView + + cellContentView.addSubview(reactionsView) + + // TODO: Use constants + let topMargin: CGFloat = 4.0 + let leftMargin: CGFloat + let rightMargin: CGFloat + + // Outgoing message + if cellData.isSenderCurrentUser { + reactionsView.alignment = .right + + // TODO: Use constants + var outgointLeftMargin: CGFloat = 80.0 + + if cellData.containsBubbleComponentWithEncryptionBadge { + outgointLeftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + leftMargin = outgointLeftMargin + + // TODO: Use constants + rightMargin = 33 + } else { + // Incoming message + + var incomingLeftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin + + if cellData.containsBubbleComponentWithEncryptionBadge { + incomingLeftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + leftMargin = incomingLeftMargin - 6.0 + + // TODO: Use constants + let messageViewMarginRight: CGFloat = 42.0 + + rightMargin = messageViewMarginRight + } + + let leadingConstraint = reactionsView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin) + + let trailingConstraint = reactionsView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -rightMargin) + + let topConstraint: NSLayoutConstraint + if let upperDecorationView = upperDecorationView { + topConstraint = reactionsView.topAnchor.constraint(equalTo: upperDecorationView.bottomAnchor, constant: topMargin) + } else { + topConstraint = reactionsView.topAnchor.constraint(equalTo: cellContentView.topAnchor, constant: contentViewPositionY + topMargin) + } + + NSLayoutConstraint.activate([ + leadingConstraint, + trailingConstraint, + topConstraint + ]) + } + + override func addURLPreviewView(_ urlPreviewView: URLPreviewView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat) { + + cell.addTemporarySubview(urlPreviewView) + + let cellContentView = cell.contentView + + urlPreviewView.translatesAutoresizingMaskIntoConstraints = false + urlPreviewView.availableWidth = cellData.maxTextViewWidth + cellContentView.addSubview(urlPreviewView) + + let leadingOrTrailingConstraint: NSLayoutConstraint + + // Outgoing message + if cellData.isSenderCurrentUser { + + // TODO: Use constants + let rightMargin: CGFloat = 34.0 + + leadingOrTrailingConstraint = urlPreviewView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -rightMargin) + } else { + // Incoming message + + var leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin + if cellData.containsBubbleComponentWithEncryptionBadge { + leftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + leftMargin-=5.0 + + leadingOrTrailingConstraint = urlPreviewView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin) + } + + let topMargin = contentViewPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin + + // Set the preview view's origin + NSLayoutConstraint.activate([ + leadingOrTrailingConstraint, + urlPreviewView.topAnchor.constraint(equalTo: cellContentView.topAnchor, constant: topMargin) + ]) + } + + // MARK: - Private + + private func createTimestampLabel(cellData: MXKRoomBubbleCellData, bubbleComponent: MXKRoomBubbleComponent, viewTag: Int) -> UILabel { + + let timeLabel = UILabel() + + timeLabel.text = cellData.eventFormatter.timeString(from: bubbleComponent.date) + timeLabel.textAlignment = .right + timeLabel.textColor = ThemeService.shared().theme.textSecondaryColor + timeLabel.font = UIFont.systemFont(ofSize: 11, weight: .light) + timeLabel.adjustsFontSizeToFitWidth = true + timeLabel.tag = viewTag + timeLabel.accessibilityIdentifier = "timestampLabel" + + return timeLabel + } + + private func canShowTimestamp(forCellData cellData: MXKRoomBubbleCellData) -> Bool { + + guard cellData.isCollapsableAndCollapsed == false else { + return false + } + + guard let firstComponent = cellData.getFirstBubbleComponentWithDisplay(), let firstEvent = firstComponent.event else { + return false + } + + switch firstEvent.eventType { + case .roomMessage: + if let messageTypeString = firstEvent.content["msgtype"] as? String { + + let messageType = MXMessageType(identifier: messageTypeString) + + switch messageType { + case .text: + return true + default: + break + } + } + default: + break + } + + return false + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.h b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.h new file mode 100644 index 000000000..a389242cc --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.h @@ -0,0 +1,25 @@ +// +// 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 "PlainRoomTimelineCellProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface BubbleRoomTimelineCellProvider : PlainRoomTimelineCellProvider + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m new file mode 100644 index 000000000..e1329b190 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m @@ -0,0 +1,56 @@ +// +// 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 "BubbleRoomTimelineCellProvider.h" + +#import "RoomOutgoingTextMsgBubbleCell.h" +#import "RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomOutgoingTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" + +#import "RoomOutgoingEncryptedTextMsgBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" + +#import "RoomOutgoingAttachmentBubbleCell.h" +#import "RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingAttachmentWithPaginationTitleBubbleCell.h" + +@implementation BubbleRoomTimelineCellProvider + +- (NSDictionary*)outgoingTextMessageCellsMapping +{ + // Hide sender info and avatar for bubble outgoing messages + return @{ + // Clear + @(RoomTimelineCellIdentifierOutgoingTextMessage) : RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo) : RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle) : RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName) : RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName) : RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierOutgoingTextMessageEncrypted) : RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo) : RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle) : RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName) : RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName) : RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + }; +} + +@end diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineStyle.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineStyle.swift new file mode 100644 index 000000000..137338e57 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineStyle.swift @@ -0,0 +1,76 @@ +// +// 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 UIKit + +class BubbleRoomTimelineStyle: RoomTimelineStyle { + + // MARK: - Properties + + // MARK: Private + + private var theme: Theme + + // MARK: Public + + let identifier: RoomTimelineStyleIdentifier + + let cellLayoutUpdater: RoomCellLayoutUpdating? + + let cellProvider: RoomTimelineCellProvider + + let cellDecorator: RoomTimelineCellDecorator + + // MARK: - Setup + + init(theme: Theme) { + self.theme = theme + self.identifier = .bubble + self.cellLayoutUpdater = BubbleRoomCellLayoutUpdater(theme: theme) + self.cellProvider = BubbleRoomTimelineCellProvider() + self.cellDecorator = BubbleRoomTimelineCellDecorator() + } + + // MARK: - Public + + func canAddEvent(_ event: MXEvent, and roomState: MXRoomState, to cellData: MXKRoomBubbleCellData) -> Bool { + return false + } + + func applySelectedStyleIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + + // Check whether the selected event belongs to this bubble + let selectedComponentIndex = cellData.selectedComponentIndex + if selectedComponentIndex != NSNotFound { + + cell.selectComponent(UInt(selectedComponentIndex), + showEditButton: false, + showTimestamp: false) + + self.cellDecorator.addTimestampLabel(toCell: cell, cellData: cellData) + } else { + cell.blurred = true + } + } + + // MARK: Themable + + func update(theme: Theme) { + self.theme = theme + self.cellLayoutUpdater?.update(theme: theme) + } + +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/MXKRoomBubbleTableViewCell+BubbleStyle.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/MXKRoomBubbleTableViewCell+BubbleStyle.swift new file mode 100644 index 000000000..a0fd3f527 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/MXKRoomBubbleTableViewCell+BubbleStyle.swift @@ -0,0 +1,30 @@ +// +// 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 + +extension MXKRoomBubbleTableViewCell { + + // Enables to get existing bubble background view + // This used while there is no dedicated cell classes for bubble style + var messageBubbleBackgroundView: RoomMessageBubbleBackgroundView? { + + let foundView = self.contentView.subviews.first { view in + return view is RoomMessageBubbleBackgroundView + } + return foundView as? RoomMessageBubbleBackgroundView + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/RoomMessageBubbleBackgroundView.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/RoomMessageBubbleBackgroundView.swift new file mode 100644 index 000000000..1408b48c7 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/RoomMessageBubbleBackgroundView.swift @@ -0,0 +1,74 @@ +// +// 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 + +class RoomMessageBubbleBackgroundView: UIView { + + // MARK: - Constant + + private enum Constants { + static let cornerRadius: CGFloat = 12.0 + } + + // MARK: - Properties + + private var heightConstraint: NSLayoutConstraint? + + // MARK: - Setup + + convenience init() { + self.init(frame: CGRect.zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.commonInit() + } + + private func commonInit() { + self.translatesAutoresizingMaskIntoConstraints = false + self.layer.masksToBounds = true + self.layer.cornerRadius = Constants.cornerRadius + } + + // MARK: - Public + + @discardableResult + func updateHeight(_ height: CGFloat) -> Bool { + if let heightConstraint = self.heightConstraint { + + guard heightConstraint.constant != height else { + return false + } + + heightConstraint.constant = height + + return true + } else { + let heightConstraint = self.heightAnchor.constraint(equalToConstant: height) + heightConstraint.isActive = true + self.heightConstraint = heightConstraint + + return true + } + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift new file mode 100644 index 000000000..d684267e0 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift @@ -0,0 +1,148 @@ +// +// 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 UIKit + +@objcMembers +class PlainRoomTimelineCellDecorator: RoomTimelineCellDecorator { + + func addTimestampLabelIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + + guard cellData.containsLastMessage && cellData.isCollapsableAndCollapsed == false else { + return + } + + // Display timestamp of the last message + self.addTimestampLabel(toCell: cell, cellData: cellData) + } + + func addTimestampLabel(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + cell.addTimestampLabel(forComponent: UInt(cellData.mostRecentComponentIndex)) + } + + func addURLPreviewView(_ urlPreviewView: URLPreviewView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat) { + cell.addTemporarySubview(urlPreviewView) + + let cellContentView = cell.contentView + + urlPreviewView.translatesAutoresizingMaskIntoConstraints = false + urlPreviewView.availableWidth = cellData.maxTextViewWidth + cellContentView.addSubview(urlPreviewView) + + var leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin + if cellData.containsBubbleComponentWithEncryptionBadge { + leftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + let topMargin = contentViewPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin + + // Set the preview view's origin + NSLayoutConstraint.activate([ + urlPreviewView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin), + urlPreviewView.topAnchor.constraint(equalTo: cellContentView.topAnchor, constant: topMargin) + ]) + } + + func addReactionView(_ reactionsView: BubbleReactionsView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) { + + cell.addTemporarySubview(reactionsView) + + if let reactionsDisplayable = cell as? BubbleCellReactionsDisplayable { + reactionsDisplayable.addReactionsView(reactionsView) + } else { + reactionsView.translatesAutoresizingMaskIntoConstraints = false + + let cellContentView = cell.contentView + + cellContentView.addSubview(reactionsView) + + var leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin + + if cellData.containsBubbleComponentWithEncryptionBadge { + leftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + let rightMargin = RoomBubbleCellLayout.reactionsViewRightMargin + let topMargin = RoomBubbleCellLayout.reactionsViewTopMargin + + // The top constraint may need to include the URL preview view + let topConstraint: NSLayoutConstraint + if let upperDecorationView = upperDecorationView { + topConstraint = reactionsView.topAnchor.constraint(equalTo: upperDecorationView.bottomAnchor, constant: topMargin) + } else { + topConstraint = reactionsView.topAnchor.constraint(equalTo: cellContentView.topAnchor, constant: contentViewPositionY + topMargin) + } + + NSLayoutConstraint.activate([ + reactionsView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin), + reactionsView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -rightMargin), + topConstraint + ]) + } + } + + func addReadReceiptsView(_ readReceiptsView: MXKReceiptSendersContainer, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) { + + cell.addTemporarySubview(readReceiptsView) + + if let readReceiptsDisplayable = cell as? BubbleCellReadReceiptsDisplayable { + readReceiptsDisplayable.addReadReceiptsView(readReceiptsView) + } else { + + let cellContentView = cell.contentView + + cellContentView.addSubview(readReceiptsView) + + // Force receipts container size + let widthConstraint = readReceiptsView.widthAnchor.constraint(equalToConstant: RoomBubbleCellLayout.readReceiptsViewWidth) + let heightConstraint = readReceiptsView.heightAnchor.constraint(equalToConstant: RoomBubbleCellLayout.readReceiptsViewHeight) + + // Force receipts container position + let trailingConstraint = readReceiptsView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -RoomBubbleCellLayout.readReceiptsViewRightMargin) + + let topMargin = RoomBubbleCellLayout.readReceiptsViewTopMargin + + let topConstraint: NSLayoutConstraint + if let upperDecorationView = upperDecorationView { + topConstraint = readReceiptsView.topAnchor.constraint(equalTo: upperDecorationView.bottomAnchor, constant: topMargin) + } else { + topConstraint = readReceiptsView.topAnchor.constraint(equalTo: cellContentView.topAnchor, constant: contentViewPositionY + topMargin) + } + + NSLayoutConstraint.activate([ + widthConstraint, + heightConstraint, + trailingConstraint, + topConstraint + ]) + } + } + + func addSendStatusView(toCell cell: MXKRoomBubbleTableViewCell, withFailedEventIds failedEventIds: Set) { + cell.updateTickView(withFailedEventIds: failedEventIds) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h new file mode 100644 index 000000000..12b1876ee --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h @@ -0,0 +1,29 @@ +// +// 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 "RoomTimelineCellProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PlainRoomTimelineCellProvider: NSObject + +- (NSDictionary*)outgoingTextMessageCellsMapping; + +- (NSDictionary*)outgoingAttachmentCellsMapping; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m new file mode 100644 index 000000000..58a460dfe --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m @@ -0,0 +1,451 @@ +// +// 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 "PlainRoomTimelineCellProvider.h" + +#import "MXKRoomBubbleTableViewCell+Riot.h" + +#import "RoomEmptyBubbleCell.h" + +#import "RoomIncomingTextMsgBubbleCell.h" +#import "RoomIncomingTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomIncomingTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomIncomingTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" +#import "RoomIncomingAttachmentBubbleCell.h" +#import "RoomIncomingAttachmentWithoutSenderInfoBubbleCell.h" +#import "RoomIncomingAttachmentWithPaginationTitleBubbleCell.h" + +#import "RoomIncomingEncryptedTextMsgBubbleCell.h" +#import "RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" +#import "RoomIncomingEncryptedAttachmentBubbleCell.h" +#import "RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.h" +#import "RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.h" + +#import "RoomOutgoingTextMsgBubbleCell.h" +#import "RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomOutgoingTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingAttachmentBubbleCell.h" +#import "RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingAttachmentWithPaginationTitleBubbleCell.h" + +#import "RoomOutgoingEncryptedTextMsgBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h" +#import "RoomOutgoingEncryptedAttachmentBubbleCell.h" +#import "RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.h" +#import "RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.h" + +#import "RoomMembershipBubbleCell.h" +#import "RoomMembershipWithPaginationTitleBubbleCell.h" +#import "RoomMembershipCollapsedBubbleCell.h" +#import "RoomMembershipCollapsedWithPaginationTitleBubbleCell.h" +#import "RoomMembershipExpandedBubbleCell.h" +#import "RoomMembershipExpandedWithPaginationTitleBubbleCell.h" +#import "RoomCreationWithPaginationCollapsedBubbleCell.h" +#import "RoomCreationCollapsedBubbleCell.h" + +#import "RoomSelectedStickerBubbleCell.h" +#import "RoomPredecessorBubbleCell.h" + +#import "GeneratedInterface-Swift.h" + +@interface PlainRoomTimelineCellProvider() + +@property (nonatomic, strong) NSDictionary* cellClasses; + +@end + +@implementation PlainRoomTimelineCellProvider + +#pragma mark - Public + +- (void)registerCellsForTableView:(UITableView*)tableView +{ + // Text message + + [self registerIncomingTextMessageCellsForTableView:tableView]; + + [self registerOutgoingTextMessageCellsForTableView:tableView]; + + // Attachment cells + + [self registerIncomingAttachmentCellsForTableView:tableView]; + + [self registerOutgoingAttachmentCellsForTableView:tableView]; + + // Other cells + + [self registerMembershipCellsForTableView:tableView]; + + [self registerKeyVerificationCellsForTableView:tableView]; + + [self registerRoomCreationCellsForTableView:tableView]; + + [self registerCallCellsForTableView:tableView]; + + [self registerVoiceMessageCellsForTableView:tableView]; + + [self registerPollCellsForTableView:tableView]; + + [self registerLocationCellsForTableView:tableView]; + + [tableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier]; + + [tableView registerClass:RoomSelectedStickerBubbleCell.class forCellReuseIdentifier:RoomSelectedStickerBubbleCell.defaultReuseIdentifier]; + + [tableView registerClass:RoomPredecessorBubbleCell.class forCellReuseIdentifier:RoomPredecessorBubbleCell.defaultReuseIdentifier]; + + [tableView registerClass:RoomCreationIntroCell.class forCellReuseIdentifier:RoomCreationIntroCell.defaultReuseIdentifier]; + + [tableView registerNib:RoomTypingBubbleCell.nib forCellReuseIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier]; +} + +- (Class)cellViewClassForCellIdentifier:(RoomTimelineCellIdentifier)identifier +{ + if (self.cellClasses == nil) + { + self.cellClasses = [self buildCellClasses]; + } + + Class cellViewClass = self.cellClasses[@(identifier)]; + + + return cellViewClass; +} + +#pragma mark - Private + +#pragma mark Cell registration + +- (void)registerIncomingTextMessageCellsForTableView:(UITableView*)tableView +{ + // Clear + + [tableView registerClass:RoomIncomingTextMsgBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + + // Encrypted + + [tableView registerClass:RoomIncomingEncryptedTextMsgBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerOutgoingTextMessageCellsForTableView:(UITableView*)tableView +{ + // Clear + + [tableView registerClass:RoomOutgoingTextMsgBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + + // Encrypted + + [tableView registerClass:RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedTextMsgBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerIncomingAttachmentCellsForTableView:(UITableView*)tableView +{ + // Clear + + [tableView registerClass:RoomIncomingAttachmentBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + // Encrypted + + [tableView registerClass:RoomIncomingEncryptedAttachmentBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerOutgoingAttachmentCellsForTableView:(UITableView*)tableView +{ + // Clear + + [tableView registerClass:RoomOutgoingAttachmentBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingAttachmentWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomOutgoingAttachmentWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + // Encrypted + + [tableView registerClass:RoomOutgoingEncryptedAttachmentBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerMembershipCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:RoomMembershipBubbleCell.class forCellReuseIdentifier:RoomMembershipBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomMembershipWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomMembershipCollapsedBubbleCell.class forCellReuseIdentifier:RoomMembershipCollapsedBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomMembershipCollapsedWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipCollapsedWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomMembershipExpandedBubbleCell.class forCellReuseIdentifier:RoomMembershipExpandedBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomMembershipExpandedWithPaginationTitleBubbleCell.class forCellReuseIdentifier:RoomMembershipExpandedWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerKeyVerificationCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:KeyVerificationIncomingRequestApprovalBubbleCell.class forCellReuseIdentifier:KeyVerificationIncomingRequestApprovalBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:KeyVerificationRequestStatusBubbleCell.class forCellReuseIdentifier:KeyVerificationRequestStatusBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:KeyVerificationRequestStatusWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationRequestStatusWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:KeyVerificationConclusionBubbleCell.class forCellReuseIdentifier:KeyVerificationConclusionBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:KeyVerificationConclusionWithPaginationTitleBubbleCell.class forCellReuseIdentifier:KeyVerificationConclusionWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerRoomCreationCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:RoomCreationCollapsedBubbleCell.class forCellReuseIdentifier:RoomCreationCollapsedBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomCreationWithPaginationCollapsedBubbleCell.class forCellReuseIdentifier:RoomCreationWithPaginationCollapsedBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerCallCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:RoomDirectCallStatusBubbleCell.class forCellReuseIdentifier:RoomDirectCallStatusBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:RoomGroupCallStatusBubbleCell.class forCellReuseIdentifier:RoomGroupCallStatusBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerVoiceMessageCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:VoiceMessageBubbleCell.class forCellReuseIdentifier:VoiceMessageBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceMessageWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceMessageWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerPollCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:PollBubbleCell.class forCellReuseIdentifier:PollBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:PollWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:PollWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:PollWithPaginationTitleBubbleCell.class forCellReuseIdentifier:PollWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerLocationCellsForTableView:(UITableView*)tableView +{ + [tableView registerClass:LocationBubbleCell.class forCellReuseIdentifier:LocationBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:LocationWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:LocationWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +#pragma mark Cell class association + +- (NSDictionary*)buildCellClasses +{ + NSMutableDictionary* cellClasses = [NSMutableDictionary dictionary]; + + // Text message + + NSDictionary *incomingTextMessageCellsMapping = [self incomingTextMessageCellsMapping]; + [cellClasses addEntriesFromDictionary:incomingTextMessageCellsMapping]; + + NSDictionary *outgoingTextMessageCellsMapping = [self incomingTextMessageCellsMapping]; + [cellClasses addEntriesFromDictionary:outgoingTextMessageCellsMapping]; + + // Attachment + + NSDictionary *incomingAttachmentCellsMapping = [self incomingAttachmentCellsMapping]; + [cellClasses addEntriesFromDictionary:incomingAttachmentCellsMapping]; + + NSDictionary *outgoingAttachmentCellsMapping = [self outgoingAttachmentCellsMapping]; + [cellClasses addEntriesFromDictionary:outgoingAttachmentCellsMapping]; + + // Other cells + + NSDictionary *roomMembershipCellsMapping = [self membershipCellsMapping]; + [cellClasses addEntriesFromDictionary:roomMembershipCellsMapping]; + + NSDictionary *keyVerificationCellsMapping = [self keyVerificationCellsMapping]; + [cellClasses addEntriesFromDictionary:keyVerificationCellsMapping]; + + NSDictionary *roomCreationCellsMapping = [self roomCreationCellsMapping]; + [cellClasses addEntriesFromDictionary:roomCreationCellsMapping]; + + NSDictionary *callCellsMapping = [self callCellsMapping]; + [cellClasses addEntriesFromDictionary:callCellsMapping]; + + NSDictionary *voiceMessageCellsMapping = [self voiceMessageCellsMapping]; + [cellClasses addEntriesFromDictionary:voiceMessageCellsMapping]; + + NSDictionary *pollCellsMapping = [self pollCellsMapping]; + [cellClasses addEntriesFromDictionary:pollCellsMapping]; + + NSDictionary *locationCellsMapping = [self locationCellsMapping]; + [cellClasses addEntriesFromDictionary:locationCellsMapping]; + + NSDictionary *othersCells = @{ + @(RoomTimelineCellIdentifierEmpty) : RoomEmptyBubbleCell.class, + @(RoomTimelineCellIdentifierSelectedSticker) : RoomSelectedStickerBubbleCell.class, + @(RoomTimelineCellIdentifierRoomPredecessor) : RoomPredecessorBubbleCell.class, + @(RoomTimelineCellIdentifierRoomCreationIntro) : RoomCreationIntroCell.class, + @(RoomTimelineCellIdentifierTyping) : RoomTypingBubbleCell.class, + }; + [cellClasses addEntriesFromDictionary:othersCells]; + + return [cellClasses copy]; +} + +- (NSDictionary*)incomingTextMessageCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierIncomingTextMessage) : RoomIncomingTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderInfo) : RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitle) : RoomIncomingTextMsgWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderName) : RoomIncomingTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitleWithoutSenderName) : RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierIncomingTextMessageEncrypted) : RoomIncomingEncryptedTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderInfo) : RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitle) : RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderName) : RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitleWithoutSenderName) : RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + }; +} + +- (NSDictionary*)outgoingTextMessageCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierOutgoingTextMessage) : RoomOutgoingTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo) : RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle) : RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName) : RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName) : RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierOutgoingTextMessageEncrypted) : RoomOutgoingEncryptedTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo) : RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle) : RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName) : RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName) : RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class, + }; +} + +- (NSDictionary*)incomingAttachmentCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierIncomingAttachment) : RoomIncomingAttachmentBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutSenderInfo) : RoomIncomingAttachmentWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithPaginationTitle) : RoomIncomingAttachmentWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierIncomingAttachmentEncrypted) : RoomIncomingEncryptedAttachmentBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithoutSenderInfo) : RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithPaginationTitle) : RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.class + }; +} + +- (NSDictionary*)outgoingAttachmentCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierOutgoingAttachment) : RoomOutgoingAttachmentBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutSenderInfo) : RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithPaginationTitle) : RoomOutgoingAttachmentWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierOutgoingAttachmentEncrypted) : RoomOutgoingEncryptedAttachmentBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo) : RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle) : RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.class + }; +} + +- (NSDictionary*)membershipCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierMembership) : RoomMembershipBubbleCell.class, + @(RoomTimelineCellIdentifierMembershipWithPaginationTitle) : RoomMembershipWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierMembershipCollapsed) : RoomMembershipCollapsedBubbleCell.class, + @(RoomTimelineCellIdentifierMembershipCollapsedWithPaginationTitle) : RoomMembershipCollapsedWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierMembershipExpanded) : RoomMembershipExpandedBubbleCell.class, + @(RoomTimelineCellIdentifierMembershipExpandedWithPaginationTitle) : RoomMembershipExpandedWithPaginationTitleBubbleCell.class, + }; +} + +- (NSDictionary*)keyVerificationCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierKeyVerificationIncomingRequestApproval) : KeyVerificationIncomingRequestApprovalBubbleCell.class, + @(RoomTimelineCellIdentifierKeyVerificationIncomingRequestApprovalWithPaginationTitle) : KeyVerificationIncomingRequestApprovalWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierKeyVerificationRequestStatus) : KeyVerificationRequestStatusBubbleCell.class, + @(RoomTimelineCellIdentifierKeyVerificationRequestStatusWithPaginationTitle) : KeyVerificationRequestStatusWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierKeyVerificationConclusion) : KeyVerificationConclusionBubbleCell.class, + @(RoomTimelineCellIdentifierKeyVerificationConclusionWithPaginationTitle) : KeyVerificationConclusionWithPaginationTitleBubbleCell.class, + }; +} + +- (NSDictionary*)roomCreationCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierRoomCreationCollapsed) : RoomCreationCollapsedBubbleCell.class, + @(RoomTimelineCellIdentifierRoomCreationCollapsedWithPaginationTitle) : RoomCreationWithPaginationCollapsedBubbleCell.class, + }; +} + +- (NSDictionary*)callCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierDirectCallStatus) : RoomDirectCallStatusBubbleCell.class, + @(RoomTimelineCellIdentifierGroupCallStatus) : RoomGroupCallStatusBubbleCell.class, + }; +} + +- (NSDictionary*)voiceMessageCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierVoiceMessage) : VoiceMessageBubbleCell.class, + @(RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo) : VoiceMessageWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle) : VoiceMessageWithPaginationTitleBubbleCell.class, + }; +} + +- (NSDictionary*)pollCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierPoll) : PollBubbleCell.class, + @(RoomTimelineCellIdentifierPollWithoutSenderInfo) : PollWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierPollWithPaginationTitle) : PollWithPaginationTitleBubbleCell.class, + }; +} + +- (NSDictionary*)locationCellsMapping +{ + return @{ + @(RoomTimelineCellIdentifierLocation) : LocationBubbleCell.class, + @(RoomTimelineCellIdentifierLocationWithoutSenderInfo) : LocationWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierLocationWithPaginationTitle) : LocationWithPaginationTitleBubbleCell.class + }; +} + + +@end diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineStyle.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineStyle.swift new file mode 100644 index 000000000..f3fba9c4e --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineStyle.swift @@ -0,0 +1,74 @@ +// +// 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 + +class PlainRoomTimelineStyle: RoomTimelineStyle { + + // MARK: - Properties + + // MARK: Private + + private var theme: Theme + + // MARK: Public + + let identifier: RoomTimelineStyleIdentifier + + let cellLayoutUpdater: RoomCellLayoutUpdating? + + let cellProvider: RoomTimelineCellProvider + + let cellDecorator: RoomTimelineCellDecorator + + // MARK: - Setup + + init(theme: Theme) { + self.theme = theme + self.identifier = .plain + self.cellLayoutUpdater = nil + self.cellProvider = PlainRoomTimelineCellProvider() + self.cellDecorator = PlainRoomTimelineCellDecorator() + } + + // MARK: - Methods + + func canAddEvent(_ event: MXEvent, and roomState: MXRoomState, to cellData: MXKRoomBubbleCellData) -> Bool { + return true + } + + func applySelectedStyleIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { + + // Check whether the selected event belongs to this bubble + let selectedComponentIndex = cellData.selectedComponentIndex + if selectedComponentIndex != NSNotFound { + + let showTimestamp = cellData.showTimestampForSelectedComponent + + cell.selectComponent(UInt(selectedComponentIndex), + showEditButton: false, + showTimestamp: showTimestamp) + } else { + cell.blurred = true + } + } + + // MARK: Themable + + func update(theme: Theme) { + self.theme = theme + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift new file mode 100644 index 000000000..71212d94d --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift @@ -0,0 +1,28 @@ +// +// 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 + +/// Enables to setup or update a room timeline cell view +@objc +protocol RoomCellLayoutUpdating: Themable { + + func updateLayoutIfNeeded(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) + + func setupLayout(forIncomingTextMessageCell cell: MXKRoomBubbleTableViewCell) + + func setupLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell) +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift new file mode 100644 index 000000000..89a665fe8 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift @@ -0,0 +1,47 @@ +// +// 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 UIKit + +/// RoomTimelineCellDecorator enables to add decoration on a cell (reactions, read receipts, timestamp, URL preview). +@objc +protocol RoomTimelineCellDecorator { + + func addTimestampLabelIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData) + + func addTimestampLabel(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) + + func addURLPreviewView(_ urlPreviewView: URLPreviewView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat) + + func addReactionView(_ reactionsView: BubbleReactionsView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) + + func addReadReceiptsView(_ readReceiptsView: MXKReceiptSendersContainer, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) + + func addSendStatusView(toCell cell: MXKRoomBubbleTableViewCell, + withFailedEventIds failedEventIds: Set) +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellProvider.h b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellProvider.h new file mode 100644 index 000000000..40037c3a2 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellProvider.h @@ -0,0 +1,36 @@ +// +// 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 + +#import "RoomTimelineCellIdentifier.h" +#import "MXKCellRendering.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Enables to register and provide room timeline cells +@protocol RoomTimelineCellProvider + +/// Register timeline cells for the given table view +- (void)registerCellsForTableView:(UITableView*)tableView; + +/// Get timeline cell class from cell identifier +- (Class)cellViewClassForCellIdentifier:(RoomTimelineCellIdentifier)identifier; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyle.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyle.swift new file mode 100644 index 000000000..b430171b5 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyle.swift @@ -0,0 +1,45 @@ +// +// 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 MatrixSDK + +/// RoomTimelineStyle describes a room timeline style used to customize timeline appearance +@objc +protocol RoomTimelineStyle: Themable { + + // MARK: - Properties + + /// Style identifier + var identifier: RoomTimelineStyleIdentifier { get } + + /// Update layout if needed for cells provided by the cell provider + var cellLayoutUpdater: RoomCellLayoutUpdating? { get } + + /// Register and provide timeline cells + var cellProvider: RoomTimelineCellProvider { get } + + /// Handle cell decorations (reactions, read receipts, URL preview, …) + var cellDecorator: RoomTimelineCellDecorator { get } + + // MARK: - Methods + + /// Indicate to merge or not event in timeline + func canAddEvent(_ event: MXEvent, and roomState: MXRoomState, to cellData: MXKRoomBubbleCellData) -> Bool + + /// Apply selected or blurred style on cell + func applySelectedStyleIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyleIdentifier.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyleIdentifier.swift index e4f9a2316..a285fe8e0 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyleIdentifier.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineStyleIdentifier.swift @@ -17,7 +17,8 @@ import Foundation /// Represents the room timeline style identifiers available -enum RoomTimelineStyleIdentifier { +@objc +enum RoomTimelineStyleIdentifier: Int { case plain case bubble } diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.h b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.h similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.h rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.h diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.m similarity index 69% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.m rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.m index 253e7141f..f4cfc616c 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.m @@ -16,6 +16,17 @@ #import "MXKRoomIncomingTextMsgBubbleCell.h" +#import "GeneratedInterface-Swift.h" + @implementation MXKRoomIncomingTextMsgBubbleCell +- (void)setupViews +{ + [super setupViews]; + + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + [timelineConfiguration.currentStyle.cellLayoutUpdater setupLayoutForIncomingTextMessageCell:self]; +} + @end diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.xib similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgBubbleCell.xib rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgBubbleCell.xib diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.h b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.h similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.h rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.h diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.m similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.m rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.m diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Incoming/Common/MXKRoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.h b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.h similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.h rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.h diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.m similarity index 69% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.m rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.m index 232813182..bf6c70ae8 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.m @@ -16,6 +16,18 @@ #import "MXKRoomOutgoingTextMsgBubbleCell.h" +#import "GeneratedInterface-Swift.h" + @implementation MXKRoomOutgoingTextMsgBubbleCell -@end \ No newline at end of file +- (void)setupViews +{ + [super setupViews]; + + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + [timelineConfiguration.currentStyle.cellLayoutUpdater setupLayoutForOutgoingTextMessageCell:self]; +} + + +@end diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.xib similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgBubbleCell.xib rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgBubbleCell.xib diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.h diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib similarity index 100% rename from Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib rename to Riot/Modules/Room/Views/BubbleCells/TextMessage/Outgoing/Common/MXKRoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 782d8a3d8..2e46a6a91 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -163,8 +163,7 @@ typedef NS_ENUM(NSUInteger, ABOUT) typedef NS_ENUM(NSUInteger, LABS_ENABLE) { - LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX, - LABS_ENABLE_POLLS + LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -579,7 +578,6 @@ TableViewSectionsDelegate> { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; - [sectionLabs addRowWithTag:LABS_ENABLE_POLLS]; sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) @@ -2464,19 +2462,6 @@ TableViewSectionsDelegate> cell = labelAndSwitchCell; } - - if (row == LABS_ENABLE_POLLS && BuildSettings.pollsEnabled) - { - MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - - labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnabledPolls]; - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowPollsAction; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnablePolls:) forControlEvents:UIControlEventValueChanged]; - - cell = labelAndSwitchCell; - } } else if (section == SECTION_TAG_FLAIR) { @@ -3209,11 +3194,6 @@ TableViewSectionsDelegate> RiotSettings.shared.enableRingingForGroupCalls = sender.isOn; } -- (void)toggleEnablePolls:(UISwitch *)sender -{ - RiotSettings.shared.roomScreenAllowPollsAction = sender.isOn; -} - - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; @@ -3898,6 +3878,13 @@ TableViewSectionsDelegate> - (void)toggleEnableRoomMessageBubbles:(UISwitch *)sender { RiotSettings.shared.roomScreenEnableMessageBubbles = sender.isOn; + + [[RoomTimelineConfiguration shared] updateStyleWithIdentifier:RiotSettings.shared.roomTimelineStyleIdentifier]; + + // Close all room data sources + // Be sure to use new room timeline style configurations + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession]; + [roomDataSourceManager reset]; } #pragma mark - TextField listener diff --git a/Riot/Modules/SideMenu/SideMenuViewModel.swift b/Riot/Modules/SideMenu/SideMenuViewModel.swift index c8baac060..7c9d128ee 100644 --- a/Riot/Modules/SideMenu/SideMenuViewModel.swift +++ b/Riot/Modules/SideMenu/SideMenuViewModel.swift @@ -97,8 +97,13 @@ final class SideMenuViewModel: SideMenuViewModelType { return } - let sideMenuItems: [SideMenuItem] = [ - .inviteFriends, + var sideMenuItems: [SideMenuItem] = [] + + if BuildSettings.sideMenuShowInviteFriends { + sideMenuItems += [.inviteFriends] + } + + sideMenuItems += [ .settings, .help, .feedback diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index c5a22e8a7..9eaae3a43 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -46,6 +46,9 @@ #import "NSArray+Element.h" #import "ShareItemSender.h" #import "HTMLFormatter.h" +#import "RoomTimelineCellProvider.h" +#import "PlainRoomTimelineCellProvider.h" +#import "BubbleRoomTimelineCellProvider.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index b9678fdbd..99f96bce8 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 999e2a95f..971929ab4 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index c9e5f44b4..6e107b62b 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt -/* - 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. - */ +// +// 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 SwiftUI diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 9c303bbbe..ed947e303 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index b8a38a117..c24e1fa63 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index 8f7acf49d..5e622ab5d 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index f64a1a51f..cedc46d0f 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -24,7 +24,7 @@ enum MockAppScreens { MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, - MockPollTimelineScreenState.self, + MockTimelinePollScreenState.self, MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, MockTemplateRoomChatScreenState.self diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index 931c6a3d7..471187dd5 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -17,7 +17,6 @@ import Foundation import UIKit import SwiftUI -import Keys struct LocationSharingCoordinatorParameters { let roomDataSource: MXKRoomDataSource diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index be9c188f9..88b0ee9cb 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -66,11 +66,11 @@ struct LocationSharingViewState: BindableState { } struct LocationSharingViewStateBindings { - var alertInfo: ErrorAlertInfo? + var alertInfo: LocationSharingErrorAlertInfo? var userLocation: CLLocationCoordinate2D? } -struct ErrorAlertInfo: Identifiable { +struct LocationSharingErrorAlertInfo: Identifiable { enum AlertType { case mapLoadingError case userLocatingError diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift index f316d761e..fd4ca6a4a 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -16,7 +16,6 @@ import Foundation import SwiftUI -import Keys import CoreLocation @available(iOS 14.0, *) @@ -25,7 +24,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { case displayExistingLocation var screenType: Any.Type { - MockLocationSharingScreenState.self + LocationSharingView.self } var screenView: ([Any], AnyView) { @@ -35,7 +34,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) } - let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! let viewModel = LocationSharingViewModel(tileServerMapURL: mapURL, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice"), location: location) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 765c0b558..40b750f85 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -72,24 +72,24 @@ class LocationSharingViewModel: LocationSharingViewModelType { switch error { case .failedLoadingMap: - state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .failedLocatingUser: - state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .invalidLocationAuthorization: - state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), - secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { - UIApplication.shared.open(applicationSettingsURL) - } - })) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) default: break } @@ -100,10 +100,10 @@ class LocationSharingViewModel: LocationSharingViewModelType { state.showLoadingIndicator = false if error != nil { - state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, nil), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) } } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift index 0727e0b5b..26bc101d3 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift @@ -25,16 +25,20 @@ struct LocationSharingUserMarkerView: View { @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var frame: CGRect = .zero + // MARK: Public let avatarData: AvatarInputProtocol var body: some View { - ZStack(alignment: .center) { + ZStack { Image(uiImage: Asset.Images.locationUserMarker.image) AvatarImage(avatarData: avatarData, size: .large) - .offset(.init(width: 0.0, height: -1.5)) + .offset(y: -1.5) } + .background(ViewFrameReader(frame: $frame)) + .padding(.bottom, frame.height) .accentColor(theme.colors.accent) } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index 6d79f5e67..b150c5bb4 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from ScreenTemplate -// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings -/* - 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. - */ +// +// 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 UIKit diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index ee0f60a78..17bcf2548 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm -/* - 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. - */ +// +// 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 UIKit @@ -22,6 +20,7 @@ import SwiftUI struct PollEditFormCoordinatorParameters { let room: MXRoom + let pollStartEvent: MXEvent? } final class PollEditFormCoordinator: Coordinator, Presentable { @@ -40,7 +39,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { } // MARK: Public - + var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? @@ -51,9 +50,20 @@ final class PollEditFormCoordinator: Coordinator, Presentable { init(parameters: PollEditFormCoordinatorParameters) { self.parameters = parameters - let viewModel = PollEditFormViewModel() - let view = PollEditForm(viewModel: viewModel.context) + var viewModel: PollEditFormViewModel + if let startEvent = parameters.pollStartEvent, + let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, + pollDetails: EditFormPollDetails(type: Self.pollKindKeyToDetailsType(pollContent.kind), + question: pollContent.question, + answerOptions: pollContent.answerOptions.map { $0.text }))) + } else { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) + } + + let view = PollEditForm(viewModel: viewModel.context) + _pollEditFormViewModel = viewModel pollEditFormHostingController = VectorHostingController(rootView: view) } @@ -70,16 +80,9 @@ final class PollEditFormCoordinator: Coordinator, Presentable { switch result { case .cancel: self.completion?() - case .create(let question, let answerOptions): - var options = [MXEventContentPollStartAnswerOption]() - for answerOption in answerOptions { - options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) - } + case .create(let details): - let pollStartContent = MXEventContentPollStart(question: question, - kind: kMXMessageContentKeyExtensiblePollKindDisclosed, - maxSelections: 1, - answerOptions: options) + let pollStartContent = self.buildPollContentWithDetails(details) self.pollEditFormViewModel.dispatch(action: .startLoading) @@ -92,15 +95,72 @@ final class PollEditFormCoordinator: Coordinator, Presentable { guard let self = self else { return } MXLog.error("Failed creating poll with error: \(String(describing: error))") - self.pollEditFormViewModel.dispatch(action: .stopLoading(error)) + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll)) } + + case .update(let details): + guard let pollStartEvent = self.parameters.pollStartEvent else { + fatalError() + } + + self.pollEditFormViewModel.dispatch(action: .startLoading) + + guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else { + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + return + } + + let newPollContent = self.buildPollContentWithDetails(details) + + self.parameters.room.sendPollUpdate(for: pollStartEvent, + oldContent: oldPollContent, + newContent: newPollContent, localEcho: nil) { [weak self] result in + guard let self = self else { return } + + self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() + } failure: { [weak self] error in + guard let self = self else { return } + + MXLog.error("Failed updating poll with error: \(String(describing: error))") + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + } } } } - // MARK: - Private + // MARK: - Presentable func toPresentable() -> UIViewController { return pollEditFormHostingController } + + // MARK: - Private + + private func buildPollContentWithDetails(_ details: EditFormPollDetails) -> MXEventContentPollStart { + var options = [MXEventContentPollStartAnswerOption]() + for answerOption in details.answerOptions { + options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) + } + + return MXEventContentPollStart(question: details.question, + kind: Self.pollDetailsTypeToKindKey(details.type), + maxSelections: NSNumber(value: details.maxSelections), + answerOptions: options) + + } + + private static func pollDetailsTypeToKindKey(_ type: EditFormPollType) -> String { + let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosed, + EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosed] + + return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosed + } + + private static func pollKindKeyToDetailsType(_ key: String) -> EditFormPollType { + let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed : EditFormPollType.disclosed, + kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed] + + return mapping[key] ?? EditFormPollType.disclosed + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 9c7d856c8..346ba3d57 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,10 +17,31 @@ import Foundation import SwiftUI +enum EditFormPollType { + case disclosed + case undisclosed +} + +struct EditFormPollDetails { + let type: EditFormPollType + let question: String + let answerOptions: [String] + let maxSelections: UInt = 1 + + static var `default`: EditFormPollDetails { + EditFormPollDetails(type: .disclosed, question: "", answerOptions: ["", ""]) + } +} + +enum PollEditFormMode { + case creation + case editing +} + enum PollEditFormStateAction { case viewAction(PollEditFormViewAction) case startLoading - case stopLoading(Error?) + case stopLoading(PollEditFormErrorAlertInfo.AlertType?) } enum PollEditFormViewAction { @@ -30,11 +49,13 @@ enum PollEditFormViewAction { case deleteAnswerOption(PollEditFormAnswerOption) case cancel case create + case update } enum PollEditFormViewModelResult { case cancel - case create(String, [String]) + case create(EditFormPollDetails) + case update(EditFormPollDetails) } struct PollEditFormQuestion { @@ -60,12 +81,14 @@ struct PollEditFormAnswerOption: Identifiable, Equatable { } struct PollEditFormViewState: BindableState { + var minAnswerOptionsCount: Int var maxAnswerOptionsCount: Int + var mode: PollEditFormMode var bindings: PollEditFormViewStateBindings var confirmationButtonEnabled: Bool { !bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= 2 + bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= minAnswerOptionsCount } var addAnswerOptionButtonEnabled: Bool { @@ -78,6 +101,18 @@ struct PollEditFormViewState: BindableState { struct PollEditFormViewStateBindings { var question: PollEditFormQuestion var answerOptions: [PollEditFormAnswerOption] + var type: EditFormPollType - var showsFailureAlert: Bool = false + var alertInfo: PollEditFormErrorAlertInfo? +} + +struct PollEditFormErrorAlertInfo: Identifiable { + enum AlertType { + case failedCreatingPoll + case failedUpdatingPoll + } + + let id: AlertType + let title: String + let subtitle: String } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift index 2e545a9d4..8d6720340 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // @@ -24,11 +22,11 @@ enum MockPollEditFormScreenState: MockScreenState, CaseIterable { case standard var screenType: Any.Type { - MockPollEditFormScreenState.self + PollEditForm.self } var screenView: ([Any], AnyView) { - let viewModel = PollEditFormViewModel() + let viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index f38ac9fc9..022b4b727 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,6 +17,11 @@ import SwiftUI import Combine +struct PollEditFormViewModelParameters { + let mode: PollEditFormMode + let pollDetails: EditFormPollDetails +} + @available(iOS 14, *) typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, PollEditFormStateAction, @@ -27,6 +30,7 @@ typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState class PollEditFormViewModel: PollEditFormViewModelType { private struct Constants { + static let minAnswerOptionsCount = 2 static let maxAnswerOptionsCount = 20 static let maxQuestionLength = 340 static let maxAnswerOptionLength = 340 @@ -42,20 +46,19 @@ class PollEditFormViewModel: PollEditFormViewModelType { // MARK: - Setup - init() { - super.init(initialViewState: Self.defaultState()) - } - - private static func defaultState() -> PollEditFormViewState { - return PollEditFormViewState( + init(parameters: PollEditFormViewModelParameters) { + let state = PollEditFormViewState( + minAnswerOptionsCount: Constants.minAnswerOptionsCount, maxAnswerOptionsCount: Constants.maxAnswerOptionsCount, + mode: parameters.mode, bindings: PollEditFormViewStateBindings( - question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength), - answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength), - PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength) - ] + question: PollEditFormQuestion(text: parameters.pollDetails.question, maxLength: Constants.maxQuestionLength), + answerOptions: parameters.pollDetails.answerOptions.map { PollEditFormAnswerOption(text: $0, maxLength: Constants.maxAnswerOptionLength) }, + type: parameters.pollDetails.type ) ) + + super.init(initialViewState: state) } // MARK: - Public @@ -65,11 +68,9 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .cancel: completion?(.cancel) case .create: - completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), - state.bindings.answerOptions.compactMap({ answerOption in - let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - }))) + completion?(.create(buildPollDetails())) + case .update: + completion?(.update(buildPollDetails())) default: dispatch(action: .viewAction(viewAction)) } @@ -92,10 +93,30 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .stopLoading(let error): state.showLoadingIndicator = false - if error != nil { - state.bindings.showsFailureAlert = true + switch error { + case .failedCreatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll, + title: VectorL10n.pollEditFormPostFailureTitle, + subtitle: VectorL10n.pollEditFormPostFailureSubtitle) + case .failedUpdatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll, + title: VectorL10n.pollEditFormUpdateFailureTitle, + subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle) + case .none: + break } break } } + + // MARK: - Private + + private func buildPollDetails() -> EditFormPollDetails { + return EditFormPollDetails(type: state.bindings.type, + question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), + answerOptions: state.bindings.answerOptions.compactMap({ answerOption in + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + })) + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift index 2780739cd..70042a696 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift index 739361197..662f12c1a 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -28,10 +26,10 @@ class PollEditFormViewModelTests: XCTestCase { var cancellables = Set() override func setUpWithError() throws { - viewModel = PollEditFormViewModel() + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) context = viewModel.context } - + func testInitialState() { XCTAssertTrue(context.question.text.isEmpty) XCTAssertFalse(context.viewState.confirmationButtonEnabled) @@ -100,14 +98,14 @@ class PollEditFormViewModelTests: XCTestCase { let thirdAnswer = " " viewModel.completion = { result in - if case PollEditFormViewModelResult.create(let resultQuestion, let resultAnswerOptions) = result { - XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), resultQuestion) + if case PollEditFormViewModelResult.create(let result) = result { + XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), result.question) // The last answer option should be automatically dropped as it's empty - XCTAssertEqual(resultAnswerOptions.count, 2) + XCTAssertEqual(result.answerOptions.count, 2) - XCTAssertEqual(resultAnswerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) - XCTAssertEqual(resultAnswerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(result.answerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(result.answerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 57f3a63a3..c4f4608e2 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -37,6 +35,9 @@ struct PollEditForm: View { ScrollView { VStack(alignment: .leading, spacing: 32.0) { + // Intentionally disabled until platform parity. + // PollEditFormTypePicker(selectedType: $viewModel.type) + VStack(alignment: .leading, spacing: 16.0) { Text(VectorL10n.pollEditFormPollQuestionOrTopic) .font(theme.fonts.title3SB) @@ -58,7 +59,7 @@ struct PollEditForm: View { ForEach(0.. Void - - var body: some View { - VStack(alignment: .leading, spacing: 8.0) { - Text(VectorL10n.pollEditFormOptionNumber(index + 1)) - .font(theme.fonts.subheadline) - .foregroundColor(theme.colors.primaryContent) - - HStack(spacing: 16.0) { - TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in - self.focused = edit - }) - .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) - Button { - onDelete() - } label: { - Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) - } - .accessibilityIdentifier("Delete answer option") - } - } - } -} - // MARK: - Previews @available(iOS 14.0, *) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift new file mode 100644 index 000000000..e3de14987 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift @@ -0,0 +1,63 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct PollEditFormAnswerOptionView: View { + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @State private var focused = false + + @Binding var text: String + + let index: Int + let onDelete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormOptionNumber(index + 1)) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + HStack(spacing: 16.0) { + TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in + self.focused = edit + }) + .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) + Button(action: onDelete) { + Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) + } + .accessibilityIdentifier("Delete answer option") + } + } + } +} + +@available(iOS 14.0, *) +struct PollEditFormAnswerOptionView_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: 32.0) { + PollEditFormAnswerOptionView(text: Binding.constant(""), index: 0) { + + } + PollEditFormAnswerOptionView(text: Binding.constant("Test"), index: 5) { + + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift new file mode 100644 index 000000000..590587d83 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift @@ -0,0 +1,98 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct PollEditFormTypePicker: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + @Binding var selectedType: EditFormPollType + + var body: some View { + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormPollType) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + PollEditFormTypeButton(type: .disclosed, selectedType: $selectedType) + PollEditFormTypeButton(type: .undisclosed, selectedType: $selectedType) + } + } +} + +@available(iOS 14.0, *) +private struct PollEditFormTypeButton: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + let type: EditFormPollType + @Binding var selectedType: EditFormPollType + + var body: some View { + Button { + selectedType = type + } label: { + HStack(alignment: .top, spacing: 8.0) { + + Image(uiImage: selectionImage) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + Text(description) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + } + } + } + } + + private var title: String { + switch type { + case .disclosed: + return VectorL10n.pollEditFormPollTypeOpen + case .undisclosed: + return VectorL10n.pollEditFormPollTypeClosed + } + } + + private var description: String { + switch type { + case .disclosed: + return VectorL10n.pollEditFormPollTypeOpenDescription + case .undisclosed: + return VectorL10n.pollEditFormPollTypeClosedDescription + } + } + + private var selectionImage: UIImage { + if type == selectedType { + return Asset.Images.pollTypeCheckboxSelected.image + } else { + return Asset.Images.pollTypeCheckboxDefault.image + } + } +} + +@available(iOS 14.0, *) +struct PollEditFormTypePicker_Previews: PreviewProvider { + static var previews: some View { + VStack { + PollEditFormTypePicker(selectedType: Binding.constant(.disclosed)) + PollEditFormTypePicker(selectedType: Binding.constant(.undisclosed)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift b/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift deleted file mode 100644 index b7752acf5..000000000 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift +++ /dev/null @@ -1,47 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -// -// 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 - -@available(iOS 14.0, *) -enum MockPollTimelineScreenState: MockScreenState, CaseIterable { - case open - case closed - - var screenType: Any.Type { - MockPollTimelineScreenState.self - } - - var screenView: ([Any], AnyView) { - let answerOptions = [TimelineAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), - TimelineAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), - TimelineAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - - let poll = TimelinePoll(question: "Question", - answerOptions: answerOptions, - closed: (self == .closed ? true : false), - totalAnswerCount: 20, - type: .disclosed, - maxAllowedSelections: 1) - - let viewModel = PollTimelineViewModel(timelinePoll: poll) - - return ([viewModel], AnyView(PollTimelineView(viewModel: viewModel.context))) - } -} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift deleted file mode 100644 index e5daad842..000000000 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// 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 SwiftUI - -@available(iOS 14.0, *) -struct PollTimelineAnswerOptionButton: View { - - // MARK: - Properties - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - - let answerOption: TimelineAnswerOption - let pollClosed: Bool - let showResults: Bool - let totalAnswerCount: UInt - let action: () -> Void - - // MARK: Public - - var body: some View { - Button(action: action) { - let rect = RoundedRectangle(cornerRadius: 4.0) - answerOptionLabel - .padding(.horizontal, 8.0) - .padding(.top, 12.0) - .padding(.bottom, 4.0) - .clipShape(rect) - .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) - .accentColor(progressViewAccentColor) - } - } - - var answerOptionLabel: some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(alignment: .top, spacing: 8.0) { - - if !pollClosed { - Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) - } - - Text(answerOption.text) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - - if pollClosed && answerOption.winner { - Spacer() - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - } - - HStack { - ProgressView(value: Double(showResults ? answerOption.count : 0), - total: Double(totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - .padding(.vertical, 8.0) - - if (showResults) { - Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) - .font(theme.fonts.footnote) - .foregroundColor(pollClosed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) - } - } - } - } - - var borderAccentColor: Color { - guard !pollClosed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent - } - - var progressViewAccentColor: Color { - guard !pollClosed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent - } -} - -@available(iOS 14.0, *) -struct PollTimelineAnswerOptionButton_Previews: PreviewProvider { - static let stateRenderer = MockPollTimelineScreenState.stateRenderer - static var previews: some View { - - Group { - VStack { - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: false, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: false, showResults: false, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: false, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: false, showResults: false, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: false, showResults: true, totalAnswerCount: 1000, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: false, showResults: false, totalAnswerCount: 1000, action: {}) - } - - VStack { - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: true, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: true, selected: true), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: true, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {}) - } - } - } -} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift similarity index 60% rename from RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 78c53acec..e54fd8013 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -1,43 +1,41 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline -/* - 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. - */ +// +// 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 SwiftUI import MatrixSDK import Combine -struct PollTimelineCoordinatorParameters { +struct TimelinePollCoordinatorParameters { let session: MXSession let room: MXRoom let pollStartEvent: MXEvent } @available(iOS 14.0, *) -final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate { +final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate { // MARK: - Properties // MARK: Private - private let parameters: PollTimelineCoordinatorParameters + private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var pollAggregator: PollAggregator - private var pollTimelineViewModel: PollTimelineViewModel! + private var viewModel: TimelinePollViewModel! private var cancellables = Set() // MARK: Public @@ -48,14 +46,14 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - Setup @available(iOS 14.0, *) - init(parameters: PollTimelineCoordinatorParameters) throws { + init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEvent: parameters.pollStartEvent) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId) pollAggregator.delegate = self - pollTimelineViewModel = PollTimelineViewModel(timelinePoll: buildTimelinePollFrom(pollAggregator.poll)) - pollTimelineViewModel.callback = { [weak self] result in + viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) + viewModel.callback = { [weak self] result in guard let self = self else { return } switch result { @@ -75,9 +73,9 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel localEcho: nil, success: nil) { [weak self] error in guard let self = self else { return } - MXLog.error("[PollTimelineCoordinator]] Failed submitting response with error \(String(describing: error))") + MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))") - self.pollTimelineViewModel.dispatch(action: .showAnsweringFailure) + self.viewModel.dispatch(action: .showAnsweringFailure) } } .store(in: &cancellables) @@ -89,23 +87,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel } func toPresentable() -> UIViewController { - return VectorHostingController(rootView: PollTimelineView(viewModel: pollTimelineViewModel.context)) + return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context)) } func canEndPoll() -> Bool { return pollAggregator.poll.isClosed == false } + func canEditPoll() -> Bool { + return false // Intentionally disabled until platform parity. + // return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + } + func endPoll() { parameters.room.sendPollEnd(for: parameters.pollStartEvent, localEcho: nil, success: nil) { [weak self] error in - self?.pollTimelineViewModel.dispatch(action: .showClosingFailure) + self?.viewModel.dispatch(action: .showClosingFailure) } } // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - pollTimelineViewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll))) + viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll))) } func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { @@ -124,20 +127,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel // PollProtocol is intentionally not available in the SwiftUI target as we don't want // to add the SDK as a dependency to it. We need to translate from one to the other on this level. - func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePoll { + func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails { let answerOptions = poll.answerOptions.map { pollAnswerOption in - TimelineAnswerOption(id: pollAnswerOption.id, + TimelinePollAnswerOption(id: pollAnswerOption.id, text: pollAnswerOption.text, count: pollAnswerOption.count, winner: pollAnswerOption.isWinner, selected: pollAnswerOption.isCurrentUserSelection) } - return TimelinePoll(question: poll.text, + return TimelinePollDetails(question: poll.text, answerOptions: answerOptions, closed: poll.isClosed, totalAnswerCount: poll.totalAnswerCount, - type: (poll.kind == .disclosed ? .disclosed : .undisclosed), - maxAllowedSelections: poll.maxAllowedSelections) + type: pollKindToTimelinePollType(poll.kind), + maxAllowedSelections: poll.maxAllowedSelections, + hasBeenEdited: poll.hasBeenEdited) + } + + private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { + let mapping = [PollKind.disclosed: TimelinePollType.disclosed, + PollKind.undisclosed: TimelinePollType.undisclosed] + + return mapping[kind] ?? .disclosed } } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift similarity index 78% rename from RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index d778cbedf..0fa488ebd 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -17,11 +17,11 @@ import Foundation @available(iOS 14, *) -class PollTimelineProvider { - static let shared = PollTimelineProvider() +class TimelinePollProvider { + static let shared = TimelinePollProvider() var session: MXSession? - var coordinatorsForEventIdentifiers = [String: PollTimelineCoordinator]() + var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() private init() { @@ -29,7 +29,7 @@ class PollTimelineProvider { /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline - func buildPollTimelineViewForEvent(_ event: MXEvent) -> UIView? { + func buildTimelinePollViewForEvent(_ event: MXEvent) -> UIView? { guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -38,8 +38,8 @@ class PollTimelineProvider { return coordinator.toPresentable().view } - let parameters = PollTimelineCoordinatorParameters(session: session, room: room, pollStartEvent: event) - guard let coordinator = try? PollTimelineCoordinator(parameters: parameters) else { + let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event) + guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { return nil } @@ -49,7 +49,7 @@ class PollTimelineProvider { } /// Retrieve the poll timeline coordinator for the given event or nil if it hasn't been created yet - func pollTimelineCoordinatorForEventIdentifier(_ eventIdentifier: String) -> PollTimelineCoordinator? { + func timelinePollCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelinePollCoordinator? { return coordinatorsForEventIdentifiers[eventIdentifier] } } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift similarity index 66% rename from RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift index f36660e54..9c4efe9c1 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -20,7 +18,7 @@ import XCTest import RiotSwiftUI @available(iOS 14.0, *) -class PollTimelineUITests: XCTestCase { +class TimelinePollUITests: XCTestCase { private var app: XCUIApplication! @@ -31,8 +29,8 @@ class PollTimelineUITests: XCTestCase { app.launch() } - func testOpenPoll() { - app.goToScreenWithIdentifier(MockPollTimelineScreenState.open.title) + func testOpenDisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.openDisclosed.title) XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["20 votes cast"].exists) @@ -69,9 +67,48 @@ class PollTimelineUITests: XCTestCase { XCTAssertEqual(app.buttons["Third, 16 votes"].value as! String, "80%") } - func testClosedPoll() { - app.goToScreenWithIdentifier(MockPollTimelineScreenState.closed.title) + func testOpenUndisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.openUndisclosed.title) + XCTAssert(app.staticTexts["Question"].exists) + XCTAssert(app.staticTexts["20 votes cast"].exists) + + XCTAssert(!app.buttons["First, 10 votes"].exists) + XCTAssert(app.buttons["First"].exists) + XCTAssertTrue((app.buttons["First"].value as! String).isEmpty) + + XCTAssert(!app.buttons["Second, 5 votes"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssertTrue((app.buttons["Second"].value as! String).isEmpty) + + XCTAssert(!app.buttons["Third, 15 votes"].exists) + XCTAssert(app.buttons["Third"].exists) + XCTAssertTrue((app.buttons["Third"].value as! String).isEmpty) + + app.buttons["First"].tap() + + XCTAssert(app.buttons["First"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssert(app.buttons["Third"].exists) + + app.buttons["Third"].tap() + + XCTAssert(app.buttons["First"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssert(app.buttons["Third"].exists) + } + + func testClosedDisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedDisclosed.title) + checkClosedPoll() + } + + func testClosedUndisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedUndisclosed.title) + checkClosedPoll() + } + + private func checkClosedPoll() { XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["Final results based on 20 votes"].exists) diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift similarity index 84% rename from RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index 9ad80e5b9..3de360418 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -22,24 +20,25 @@ import Combine @testable import RiotSwiftUI @available(iOS 14.0, *) -class PollTimelineViewModelTests: XCTestCase { - var viewModel: PollTimelineViewModel! - var context: PollTimelineViewModelType.Context! +class TimelinePollViewModelTests: XCTestCase { + var viewModel: TimelinePollViewModel! + var context: TimelinePollViewModelType.Context! var cancellables = Set() override func setUpWithError() throws { - let answerOptions = [TimelineAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), - TimelineAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), - TimelineAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] - let timelinePoll = TimelinePoll(question: "Question", - answerOptions: answerOptions, - closed: false, - totalAnswerCount: 3, - type: .disclosed, - maxAllowedSelections: 1) + let timelinePoll = TimelinePollDetails(question: "Question", + answerOptions: answerOptions, + closed: false, + totalAnswerCount: 3, + type: .disclosed, + maxAllowedSelections: 1, + hasBeenEdited: false) - viewModel = PollTimelineViewModel(timelinePoll: timelinePoll) + viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift similarity index 56% rename from RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index d01eaf864..da8abd7eb 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -19,20 +17,20 @@ import Foundation import SwiftUI -typealias PollTimelineViewModelCallback = ((PollTimelineViewModelResult) -> Void) +typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void) -enum PollTimelineStateAction { - case viewAction(PollTimelineViewAction, PollTimelineViewModelCallback?) - case updateWithPoll(TimelinePoll) +enum TimelinePollStateAction { + case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?) + case updateWithPoll(TimelinePollDetails) case showAnsweringFailure case showClosingFailure } -enum PollTimelineViewAction { +enum TimelinePollViewAction { case selectAnswerOptionWithIdentifier(String) } -enum PollTimelineViewModelResult { +enum TimelinePollViewModelResult { case selectedAnswerOptionsWithIdentifiers([String]) } @@ -41,7 +39,7 @@ enum TimelinePollType { case undisclosed } -class TimelineAnswerOption: Identifiable { +class TimelinePollAnswerOption: Identifiable { var id: String var text: String var count: UInt @@ -57,35 +55,59 @@ class TimelineAnswerOption: Identifiable { } } -class TimelinePoll { +class TimelinePollDetails { var question: String - var answerOptions: [TimelineAnswerOption] + var answerOptions: [TimelinePollAnswerOption] var closed: Bool var totalAnswerCount: UInt var type: TimelinePollType var maxAllowedSelections: UInt + var hasBeenEdited: Bool = true - init(question: String, answerOptions: [TimelineAnswerOption], closed: Bool, totalAnswerCount: UInt, type: TimelinePollType, maxAllowedSelections: UInt) { + init(question: String, answerOptions: [TimelinePollAnswerOption], + closed: Bool, + totalAnswerCount: UInt, + type: TimelinePollType, + maxAllowedSelections: UInt, + hasBeenEdited: Bool) { self.question = question self.answerOptions = answerOptions self.closed = closed self.totalAnswerCount = totalAnswerCount self.type = type self.maxAllowedSelections = maxAllowedSelections + self.hasBeenEdited = hasBeenEdited } var hasCurrentUserVoted: Bool { answerOptions.filter { $0.selected == true}.count > 0 } + + var shouldDiscloseResults: Bool { + if closed { + return totalAnswerCount > 0 + } else { + return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted + } + } } -struct PollTimelineViewState: BindableState { - var poll: TimelinePoll - var bindings: PollTimelineViewStateBindings +struct TimelinePollViewState: BindableState { + var poll: TimelinePollDetails + var bindings: TimelinePollViewStateBindings } -struct PollTimelineViewStateBindings { - var showsAnsweringFailureAlert: Bool = false - var showsClosingFailureAlert: Bool = false +struct TimelinePollViewStateBindings { + var alertInfo: TimelinePollErrorAlertInfo? } +struct TimelinePollErrorAlertInfo: Identifiable { + enum AlertType { + case failedClosingPoll + case failedSubmittingAnswer + } + + let id: AlertType + let title: String + let subtitle: String +} diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift new file mode 100644 index 000000000..3fe93f8b8 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -0,0 +1,48 @@ +// +// 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 + +@available(iOS 14.0, *) +enum MockTimelinePollScreenState: MockScreenState, CaseIterable { + case openDisclosed + case closedDisclosed + case openUndisclosed + case closedUndisclosed + + var screenType: Any.Type { + TimelinePollDetails.self + } + + var screenView: ([Any], AnyView) { + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + + let poll = TimelinePollDetails(question: "Question", + answerOptions: answerOptions, + closed: (self == .closedDisclosed || self == .closedUndisclosed ? true : false), + totalAnswerCount: 20, + type: (self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed), + maxAllowedSelections: 1, + hasBeenEdited: false) + + let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + + return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift similarity index 68% rename from RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index eb802509b..f28c7185c 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -20,11 +18,11 @@ import SwiftUI import Combine @available(iOS 14, *) -typealias PollTimelineViewModelType = StateStoreViewModel +typealias TimelinePollViewModelType = StateStoreViewModel @available(iOS 14, *) -class PollTimelineViewModel: PollTimelineViewModelType { +class TimelinePollViewModel: TimelinePollViewModelType { // MARK: - Properties @@ -32,24 +30,24 @@ class PollTimelineViewModel: PollTimelineViewModelType { // MARK: Public - var callback: PollTimelineViewModelCallback? + var callback: TimelinePollViewModelCallback? // MARK: - Setup - init(timelinePoll: TimelinePoll) { - super.init(initialViewState: PollTimelineViewState(poll: timelinePoll, bindings: PollTimelineViewStateBindings())) + init(timelinePollDetails: TimelinePollDetails) { + super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings())) } // MARK: - Public - override func process(viewAction: PollTimelineViewAction) { + override func process(viewAction: TimelinePollViewAction) { switch viewAction { case .selectAnswerOptionWithIdentifier(_): dispatch(action: .viewAction(viewAction, callback)) } } - override class func reducer(state: inout PollTimelineViewState, action: PollTimelineStateAction) { + override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) { switch action { case .viewAction(let viewAction, let callback): switch viewAction { @@ -69,15 +67,19 @@ class PollTimelineViewModel: PollTimelineViewModelType { case .updateWithPoll(let poll): state.poll = poll case .showAnsweringFailure: - state.bindings.showsAnsweringFailureAlert = true + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer, + title: VectorL10n.pollTimelineVoteNotRegisteredTitle, + subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle) case .showClosingFailure: - state.bindings.showsClosingFailureAlert = true + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll, + title: VectorL10n.pollTimelineNotClosedTitle, + subtitle: VectorL10n.pollTimelineNotClosedSubtitle) } } // MARK: - Private - static func updateSingleSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) { + static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { for answerOption in state.poll.answerOptions { if answerOption.selected { answerOption.selected = false @@ -98,7 +100,7 @@ class PollTimelineViewModel: PollTimelineViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func updateMultiSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) { + static func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 @@ -122,7 +124,7 @@ class PollTimelineViewModel: PollTimelineViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func informCoordinatorOfSelectionUpdate(state: PollTimelineViewState, callback: PollTimelineViewModelCallback?) { + static func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in answerOption.selected ? answerOption.id : nil } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift new file mode 100644 index 000000000..7cd02911d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -0,0 +1,157 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct TimelinePollAnswerOptionButton: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + let poll: TimelinePollDetails + let answerOption: TimelinePollAnswerOption + let action: () -> Void + + // MARK: Public + + var body: some View { + Button(action: action) { + let rect = RoundedRectangle(cornerRadius: 4.0) + answerOptionLabel + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8.0) + .padding(.top, 12.0) + .padding(.bottom, 12.0) + .clipShape(rect) + .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) + .accentColor(progressViewAccentColor) + } + } + + var answerOptionLabel: some View { + VStack(alignment: .leading, spacing: 12.0) { + HStack(alignment: .top, spacing: 8.0) { + + if !poll.closed { + Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) + } + + Text(answerOption.text) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + + if poll.closed && answerOption.winner { + Spacer() + Image(uiImage: Asset.Images.pollWinnerIcon.image) + } + } + + if poll.type == .disclosed || poll.closed { + HStack { + ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), + total: Double(poll.totalAnswerCount)) + .progressViewStyle(LinearProgressViewStyle()) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + + if (poll.shouldDiscloseResults) { + Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) + .font(theme.fonts.footnote) + .foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) + } + } + } + } + } + + var borderAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent + } + + var progressViewAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent + } +} + +@available(iOS 14.0, *) +struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { + static let stateRenderer = MockTimelinePollScreenState.stateRenderer + + static var previews: some View { + Group { + let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed] + + ForEach(pollTypes, id: \.self) { type in + VStack { + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: true), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: true), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: true), + action: {}) + + let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(text: longText, selected: true, winner: true), + action: {}) + } + } + } + } + + static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { + TimelinePollDetails(question: "", + answerOptions: [], + closed: closed, + totalAnswerCount: 100, + type: type, + maxAllowedSelections: 1, + hasBeenEdited: false) + } + + static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { + TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift similarity index 60% rename from RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 70efcc067..89efbeb17 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,7 +17,7 @@ import SwiftUI @available(iOS 14.0, *) -struct PollTimelineView: View { +struct TimelinePollView: View { // MARK: - Properties @@ -29,29 +27,26 @@ struct PollTimelineView: View { // MARK: Public - @ObservedObject var viewModel: PollTimelineViewModel.Context + @ObservedObject var viewModel: TimelinePollViewModel.Context var body: some View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { + Text(poll.question) .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + + Text(editedText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) VStack(spacing: 24.0) { ForEach(poll.answerOptions) { answerOption in - PollTimelineAnswerOptionButton(answerOption: answerOption, - pollClosed: poll.closed, - showResults: shouldDiscloseResults, - totalAnswerCount: poll.totalAnswerCount) { + TimelinePollAnswerOptionButton(poll: poll, answerOption: answerOption) { viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id)) } } - .alert(isPresented: $viewModel.showsClosingFailureAlert) { - Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle), - message: Text(VectorL10n.pollTimelineNotClosedSubtitle), - dismissButton: .default(Text(VectorL10n.ok))) - } } .disabled(poll.closed) .fixedSize(horizontal: false, vertical: true) @@ -59,14 +54,14 @@ struct PollTimelineView: View { Text(totalVotesString) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) - .alert(isPresented: $viewModel.showsAnsweringFailureAlert) { - Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle), - message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle), - dismissButton: .default(Text(VectorL10n.ok))) - } } .padding([.horizontal, .top], 2.0) .padding([.bottom]) + .alert(item: $viewModel.alertInfo) { info in + Alert(title: Text(info.title), + message: Text(info.subtitle), + dismissButton: .default(Text(VectorL10n.ok))) + } } private var totalVotesString: String { @@ -84,32 +79,26 @@ struct PollTimelineView: View { case 0: return VectorL10n.pollTimelineTotalNoVotes case 1: - return (poll.hasCurrentUserVoted ? + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? VectorL10n.pollTimelineTotalOneVote : VectorL10n.pollTimelineTotalOneVoteNotVoted) default: - return (poll.hasCurrentUserVoted ? + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) } } - private var shouldDiscloseResults: Bool { - let poll = viewModel.viewState.poll - - if poll.closed { - return poll.totalAnswerCount > 0 - } else { - return poll.type == .disclosed && poll.totalAnswerCount > 0 && poll.hasCurrentUserVoted - } + private var editedText: String { + viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" } } // MARK: - Previews @available(iOS 14.0, *) -struct PollTimelineView_Previews: PreviewProvider { - static let stateRenderer = MockPollTimelineScreenState.stateRenderer +struct TimelinePollView_Previews: PreviewProvider { + static let stateRenderer = MockTimelinePollScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index d960a9796..4163d0668 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -/* - 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. - */ +// +// 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 UIKit diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift index dbad53c94..bd9d54e8b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift index 153848943..f21630348 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift index 6352ed5ae..d08fa62ec 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift index 0336e5be0..b15a983b6 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift index b47e436c4..0b992b6d1 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift index 7d4180201..16941ab76 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // @@ -26,7 +24,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { static private var members: [RoomMembersProviderMember]! var screenType: Any.Type { - MockUserSuggestionScreenState.self + UserSuggestionList.self } var screenView: ([Any], AnyView) { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index 1491aa579..a2a59ec86 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 42faeb6c7..44009d0c8 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index c0fa3c926..af864f6a7 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index fce852059..a8eaf6e45 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index a8c400a1f..4063e75a9 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index dcfa53fa8..dd9068fdc 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift index 3d7dca491..62ba522e4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift index 10207210c..169b89735 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift index 06b8a6518..eeb9e0b00 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from ScreenTemplate -// $ createScreen.sh Settings/Notifications NotificationSettings -/* - 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. - */ +// +// 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 diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index d7af25881..be453b948 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from FlowTemplate -// $ createRootCoordinator.sh TemplateRoomsCoordinator TemplateRooms -/* - 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. - */ +// +// 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 UIKit diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift index 8454b3ee2..dd9cd4295 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift @@ -1,20 +1,18 @@ -// File created from FlowTemplate -// $ createRootCoordinator.sh TemplateRoomsCoordinator TemplateRooms -/* - 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. - */ +// +// 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 diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift index d417c315b..a61fef6c3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift @@ -35,7 +35,7 @@ struct TemplateRoomListRow: View { AvatarImage(avatarData: avatar, size: .medium) Text(displayName ?? "") .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "roomNameText") + .accessibility(identifier: "roomNameText") Spacer() } //add to a style diff --git a/changelog.d/4208.bugfix b/changelog.d/4208.bugfix new file mode 100644 index 000000000..135c79d86 --- /dev/null +++ b/changelog.d/4208.bugfix @@ -0,0 +1 @@ +Fixed home screen not updating properly on theme changes. \ No newline at end of file diff --git a/changelog.d/5208.feature b/changelog.d/5208.feature new file mode 100644 index 000000000..1883900f3 --- /dev/null +++ b/changelog.d/5208.feature @@ -0,0 +1 @@ +Message bubbles: Text message layout. \ No newline at end of file diff --git a/changelog.d/5212.feature b/changelog.d/5212.feature new file mode 100644 index 000000000..abb4530c7 --- /dev/null +++ b/changelog.d/5212.feature @@ -0,0 +1 @@ +Message Bubbles: Support URL Previews. \ No newline at end of file diff --git a/changelog.d/5214.feature b/changelog.d/5214.feature new file mode 100644 index 000000000..31d4c5a0e --- /dev/null +++ b/changelog.d/5214.feature @@ -0,0 +1 @@ +Message Bubbles: Support reactions. \ No newline at end of file diff --git a/changelog.d/5294.misc b/changelog.d/5294.misc new file mode 100644 index 000000000..c1a31bb4a --- /dev/null +++ b/changelog.d/5294.misc @@ -0,0 +1 @@ +Fix graphql warnings in issue workflow automation diff --git a/changelog.d/5298.feature b/changelog.d/5298.feature new file mode 100644 index 000000000..605f18e07 --- /dev/null +++ b/changelog.d/5298.feature @@ -0,0 +1 @@ +Added static location sharing sending and rendering support. \ No newline at end of file diff --git a/changelog.d/5399.bugfix b/changelog.d/5399.bugfix new file mode 100644 index 000000000..015ecfc41 --- /dev/null +++ b/changelog.d/5399.bugfix @@ -0,0 +1 @@ +Fix crash when uploading a video on iPad when "Confirm size when sending" is enabled in settings. diff --git a/changelog.d/5402.bugfix b/changelog.d/5402.bugfix new file mode 100644 index 000000000..44e0c482e --- /dev/null +++ b/changelog.d/5402.bugfix @@ -0,0 +1 @@ +Fix BuildSetting to show/hide the "Invite Friends" button in the side SideMenu. diff --git a/changelog.d/5404.bugfix b/changelog.d/5404.bugfix new file mode 100644 index 000000000..1c829a4f0 --- /dev/null +++ b/changelog.d/5404.bugfix @@ -0,0 +1 @@ +Add BuildSetting to hide social login in favour of the simple SSO button.