diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a41cc47d6..4dbfca06d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: 🐞 Bug report about: Create a report to help us improve title: '' labels: 0. Needs triage, bug diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8af802020..27a64d7ed 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,9 @@ # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT -blank_issues_enabled: false \ No newline at end of file +contact_links: + - name: 🗺️ Feature request for the full Nextcloud Talk suite + url: https://github.com/nextcloud/spreed/issues/new/choose + about: Suggest an idea for Nextcloud Talk for all users, client and server + - name: ❓ Community Support and Help + url: https://help.nextcloud.com/ + about: Configuration, webserver/proxy or performance issues and other questions diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index d84da8df7..06dca0485 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: 🚀 Feature request specifically for Nextcloud Talk iOS about: Suggest an idea for Nextcloud Talk iOS title: '' labels: 0. Needs triage, enhancement diff --git a/.github/workflows/localizable.yml b/.github/workflows/localizable.yml index 7d2621589..69d63fb5b 100644 --- a/.github/workflows/localizable.yml +++ b/.github/workflows/localizable.yml @@ -25,7 +25,7 @@ permissions: jobs: check-localizable: name: Check for localizable changes - runs-on: macOS-12 + runs-on: macOS-15 steps: - name: Checkout app @@ -43,11 +43,11 @@ jobs: - name: Install dependencies run: | - pip3 install pyspelling + brew install aspell pyspelling - name: Spell check run: | - python3 -m pyspelling + pyspelling - name: Verify Changed files uses: tj-actions/verify-changed-files@v20 diff --git a/.github/workflows/pr-feedback.yml b/.github/workflows/pr-feedback.yml index 6a01fa09b..98d2715e2 100644 --- a/.github/workflows/pr-feedback.yml +++ b/.github/workflows/pr-feedback.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: The get-github-handles-from-website action - uses: marcelklehr/get-github-handles-from-website-action@a739600f6b91da4957f51db0792697afbb2f143c # v1.0.0 + uses: marcelklehr/get-github-handles-from-website-action@06b2239db0a48fe1484ba0bfd966a3ab81a08308 # v1.0.1 id: scrape with: website: 'https://nextcloud.com/team/' diff --git a/.github/workflows/uitests.yml b/.github/workflows/uitests.yml index e380e2d79..4e2001a28 100644 --- a/.github/workflows/uitests.yml +++ b/.github/workflows/uitests.yml @@ -51,9 +51,14 @@ jobs: restore-keys: | ${{ runner.os }}-pods- + - name: Setup Cocoapods + uses: maxim-lobanov/setup-cocoapods@v1.4.0 + with: + version: latest + - name: Set up dependencies talk-ios run: | - pod install + pod install - name: Build NextcloudTalk iOS for testing run: | diff --git a/.gitignore b/.gitignore index 1b3c1adff..03ac12c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ ThirdParty/WebRTC.xcframework testResult.xcresult dictionary.dic DerivedData +build diff --git a/.pyspelling.wordlist.txt b/.pyspelling.wordlist.txt index 954e8b6c5..e71a69a01 100644 --- a/.pyspelling.wordlist.txt +++ b/.pyspelling.wordlist.txt @@ -17,3 +17,7 @@ CallKit Unban unban Zammad +Strikethrough +Unarchive +unarchive +unarchived diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index d27d1a5a2..206690971 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -58,10 +58,20 @@ 1F1DF8442C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; }; 1F1DF8452C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; }; 1F1DF8462C64006E00E5EA86 /* SignalingParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */; }; + 1F20582A2CEA404F00AAA673 /* AiSummaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */; }; + 1F20582C2CEA405700AAA673 /* AiSummaryViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */; }; + 1F205BA02CEE1B8F00AAA673 /* AiSummaryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */; }; + 1F205C502CEF903000AAA673 /* UserAbsence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */; }; + 1F205C512CEF91C500AAA673 /* UserAbsence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */; }; + 1F205C522CEF91C500AAA673 /* UserAbsence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */; }; + 1F205C532CEF91C500AAA673 /* UserAbsence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */; }; + 1F205C552CEFA01200AAA673 /* OutOfOfficeView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F205C542CEFA01200AAA673 /* OutOfOfficeView.xib */; }; + 1F205C572CEFA01900AAA673 /* OutOfOfficeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205C562CEFA01900AAA673 /* OutOfOfficeView.swift */; }; + 1F205D412CFC6DD300AAA673 /* AvatarProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */; }; + 1F205D442CFC70AD00AAA673 /* AvatarProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */; }; + 1F21A0502C747FC500ED8C0C /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */; }; 1F24B5A228E0648600654457 /* ReferenceGithubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F24B5A128E0648600654457 /* ReferenceGithubView.swift */; }; 1F24B5A428E0649200654457 /* ReferenceGithubView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F24B5A328E0649200654457 /* ReferenceGithubView.xib */; }; - 1F35F8E02AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */; }; - 1F35F8E12AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */; }; 1F35F8E22AEEBAF900044BDA /* InputbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5A24322ADA77DA009939FE /* InputbarViewController.swift */; }; 1F35F8E32AEEBBE000044BDA /* NCChatTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7381552106136000CDB8DB /* NCChatTitleView.m */; }; 1F35F8E42AEEBBE500044BDA /* NCChatTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C738157210613A200CDB8DB /* NCChatTitleView.xib */; }; @@ -180,10 +190,7 @@ 1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6112AB9B161007B6037 /* ScreenCaptureController.m */; }; 1F77A6172AB9B161007B6037 /* ScreenCapturer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6132AB9B161007B6037 /* ScreenCapturer.m */; }; 1F77A6222AB9EB06007B6037 /* SocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6212AB9EB06007B6037 /* SocketConnection.m */; }; - 1F77A6242ABA0003007B6037 /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6232ABA0003007B6037 /* SampleHandler.swift */; }; 1F77A6272ABA0CD9007B6037 /* NCScreensharingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F77A6262ABA0CD9007B6037 /* NCScreensharingController.m */; }; - 1F77A62E2ABAFCC0007B6037 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */; }; - 1F77A62F2ABAFCEB007B6037 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */; }; 1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */; }; 1F785DDE2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */; }; 1F7AE07829142CA1009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07729142CA1009F72AD /* NextcloudKit */; }; @@ -262,6 +269,10 @@ 1FC4B3312CC057B700D28138 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 1FC4B3302CC057B700D28138 /* MBProgressHUD */; }; 1FC4B3332CC057DD00D28138 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 1FC4B3322CC057DD00D28138 /* MBProgressHUD */; }; 1FC4B3362CC0586A00D28138 /* libPhoneNumber in Frameworks */ = {isa = PBXBuildFile; productRef = 1FC4B3352CC0586A00D28138 /* libPhoneNumber */; }; + 1FC4B3422CCE670400D28138 /* OcsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC4B3412CCE670400D28138 /* OcsError.swift */; }; + 1FC4B3432CCE671800D28138 /* OcsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC4B3412CCE670400D28138 /* OcsError.swift */; }; + 1FC4B3442CCE671800D28138 /* OcsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC4B3412CCE670400D28138 /* OcsError.swift */; }; + 1FC4B3452CCE671800D28138 /* OcsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC4B3412CCE670400D28138 /* OcsError.swift */; }; 1FC940B92A5F21FC00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; }; 1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */; }; 1FCE3D532C9B5918009C68A9 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = 1FCE3D522C9B5918009C68A9 /* SwiftyGif */; }; @@ -279,18 +290,16 @@ 1FDCC3ED29EC7E6700DEB39B /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */; }; 1FDCC3EE29EC7E8500DEB39B /* AvatarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */; }; 1FDCC3F029ECB4CE00DEB39B /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */; }; - 1FDDB0D92AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */; }; - 1FDDB0DB2AF440E100FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */; }; 1FDE7C9A28DE14A200CB718E /* ReferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9928DE14A200CB718E /* ReferenceView.swift */; }; 1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */; }; 1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; 1FDFC94E2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; 1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; 1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */; }; + 1FE029BA2CE289A400C0C633 /* MessageSeparatorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE029B92CE289A200C0C633 /* MessageSeparatorTableViewCell.swift */; }; 1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */; }; 1FE0C56E2A0531270083576A /* ReferenceTalkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */; }; - 1FE7DE302BB4598F0040EE12 /* RoomInvitationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */; }; - 1FE7DE322BB459B10040EE12 /* RoomInvitationViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */; }; + 1FE7DE302BB4598F0040EE12 /* InfoLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7DE2F2BB4598F0040EE12 /* InfoLabelTableViewCell.swift */; }; 1FE7DE332BBC8FA00040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; }; 1FE7DE342BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; }; 1FE7DE352BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FADECD42B7EACCB007AD94B /* PrivacyInfo.xcprivacy */; }; @@ -317,9 +326,6 @@ 1FF1361C2BFBC86A006A6101 /* SwiftyAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 1FF1361B2BFBC86A006A6101 /* SwiftyAttributes */; }; 1FF2FD5D2AB99CCB000C9905 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */; }; 1FF2FD722AB99CCB000C9905 /* BroadcastUploadExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 1FF2FD7F2AB99E4D000C9905 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */; }; - 1FF2FD802AB99E4D000C9905 /* SampleUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */; }; - 1FF2FD822AB99E4D000C9905 /* SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */; }; 1FF2FD832AB99F3B000C9905 /* NCAppBranding.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A788D2359CC8800EEB797 /* NCAppBranding.m */; }; 1FF2FD852AB99F51000C9905 /* NCUserStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78E9E225120DE500E3D4CA /* NCUserStatus.m */; }; 1FF2FD862AB99F5B000C9905 /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; }; @@ -367,13 +373,7 @@ 2C06BF5D20A89F510031EB46 /* NCRoomsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF5C20A89F510031EB46 /* NCRoomsManager.m */; }; 2C06BF6420AC64370031EB46 /* DateHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C06BF6320AC64370031EB46 /* DateHeaderView.xib */; }; 2C06BF6720AC647A0031EB46 /* DateHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF6620AC647A0031EB46 /* DateHeaderView.m */; }; - 2C06BF6C20AEB0030031EB46 /* RoundedNumberView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */; }; 2C16A82C28E7284D00EDE523 /* NCButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C16A82B28E7284D00EDE523 /* NCButton.swift */; }; - 2C1ABD8625769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */; }; - 2C1ABD8725769E7D00AEDFB6 /* ShareItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */; }; - 2C1ABD8825769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */; }; - 2C1ABD8925769E7D00AEDFB6 /* ShareItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */; }; - 2C1ABD9925769F7500AEDFB6 /* ShareItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */; }; 2C1ABDC6257A7CF000AEDFB6 /* NCContactsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDC5257A7CF000AEDFB6 /* NCContactsManager.m */; }; 2C1ABDCE257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; }; 2C1ABDCF257E939600AEDFB6 /* NCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABDCD257E939600AEDFB6 /* NCContact.m */; }; @@ -389,8 +389,6 @@ 2C2E64251F3462AF00D39CE8 /* NCSignalingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C2E64241F3462AF00D39CE8 /* NCSignalingMessage.m */; }; 2C3195BC24C599130066F221 /* PlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC7158820B837140045C789 /* PlaceholderView.xib */; }; 2C3195BE24C5A7410066F221 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CA1CCAB1F067F35002FE6A2 /* Images.xcassets */; }; - 2C3195C224C5E2100066F221 /* ShareTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */; }; - 2C3195C324C5E2100066F221 /* ShareTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */; }; 2C330372255E6EBC00BDB4E4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C330374255E6EBC00BDB4E4 /* InfoPlist.strings */; }; 2C36A04A261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */; }; 2C3780BD2107209C003F9AE8 /* NCRoomParticipants.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3780BC2107209C003F9AE8 /* NCRoomParticipants.m */; }; @@ -426,18 +424,12 @@ 2C444703265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; }; 2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; }; 2C444705265D641300DF1DBC /* NCUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C444702265D641300DF1DBC /* NCUserDefaults.m */; }; - 2C444706265E59B100DF1DBC /* ShareConfirmationCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */; }; - 2C444707265E59B500DF1DBC /* ShareConfirmationCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */; }; - 2C444708265E59BC00DF1DBC /* ShareItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */; }; 2C44B4D127FF05A000AD1C86 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C44B4D027FF05A000AD1C86 /* ReactionsSummaryView.swift */; }; 2C4747E22CB58FD2002828F2 /* PollMessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C4747E12CB58FD2002828F2 /* PollMessageView.xib */; }; 2C4747E62CB6711F002828F2 /* BaseChatTableViewCell+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4747E52CB6710F002828F2 /* BaseChatTableViewCell+Poll.swift */; }; 2C4747E92CB67177002828F2 /* PollMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4747E82CB67177002828F2 /* PollMessageView.swift */; }; 2C477C1628B79D980044DEB4 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2C477C1828B79D980044DEB4 /* Localizable.stringsdict */; }; 2C4987BD21E640E20060AC27 /* CallKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4987BC21E640E20060AC27 /* CallKitManager.m */; }; - 2C4CDCD026A84AEA0023F403 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C62AFB824C1A4E6007E460A /* ShareViewController.m */; }; - 2C4CDCD126A84E500023F403 /* ShareTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */; }; - 2C4CDCD226A84E550023F403 /* ShareTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */; }; 2C4D7D631F2F7C2C00FF4A0D /* ARDCaptureController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D621F2F7C2C00FF4A0D /* ARDCaptureController.m */; }; 2C4D7D691F2F7DBC00FF4A0D /* ARDSettingsModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D651F2F7DBC00FF4A0D /* ARDSettingsModel.m */; }; 2C4D7D6A1F2F7DBC00FF4A0D /* ARDSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C4D7D681F2F7DBC00FF4A0D /* ARDSettingsStore.m */; }; @@ -458,8 +450,6 @@ 2C5BFBFE2891C3DF00E75118 /* PollResultsDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5BFBFD2891C3DF00E75118 /* PollResultsDetailsViewController.swift */; }; 2C604BD9211988A700D34DCD /* SystemMessageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */; }; 2C62AFAE24C08845007E460A /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 2C62AFA324C08845007E460A /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 2C62AFB624C1A449007E460A /* Share.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2C62AFB524C1A449007E460A /* Share.storyboard */; }; - 2C62AFB924C1A4E6007E460A /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C62AFB824C1A4E6007E460A /* ShareViewController.m */; }; 2C62AFBB24C1B7B1007E460A /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; }; 2C62AFFA24C1BDA5007E460A /* NCChatMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA15540208E350300CE8EF0 /* NCChatMessage.m */; }; 2C62AFFD24C1BDA5007E460A /* NCMessageParameter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C43BA7521309A1000B3068A /* NCMessageParameter.m */; }; @@ -478,6 +468,7 @@ 2C6955152B0CE1A30070F6E1 /* NCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */; }; 2C6E74462386D33200AE396C /* ReplyMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E74452386D33200AE396C /* ReplyMessageView.m */; }; 2C6E7449238C1A0800AE396C /* QuotedMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E7448238C1A0800AE396C /* QuotedMessageView.m */; }; + 2C6F0D6C2CDE26B0005E0812 /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6F0D6B2CDE26B0005E0812 /* BadgeView.swift */; }; 2C7381562106136000CDB8DB /* NCChatTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7381552106136000CDB8DB /* NCChatTitleView.m */; }; 2C738158210613A200CDB8DB /* NCChatTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C738157210613A200CDB8DB /* NCChatTitleView.xib */; }; 2C78E9E325120DE600E3D4CA /* NCUserStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C78E9E225120DE500E3D4CA /* NCUserStatus.m */; }; @@ -495,7 +486,6 @@ 2C8A2BC9221F094F00DE6D2C /* DirectoryTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8A2BC8221F094F00DE6D2C /* DirectoryTableViewController.m */; }; 2C8A2BCF221FEEFE00DE6D2C /* DirectoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C8A2BCD221FEEFE00DE6D2C /* DirectoryTableViewCell.xib */; }; 2C8CDD0621C2EDE8004E2997 /* AvatarBackgroundImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8CDD0521C2EDE8004E2997 /* AvatarBackgroundImageView.m */; }; - 2C8E2A1B232174C20022BFC9 /* MessageSeparatorTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */; }; 2C90E5641EDDE0FB0093D85A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5631EDDE0FB0093D85A /* Foundation.framework */; }; 2C90E5671EDDE1340093D85A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5661EDDE1340093D85A /* CoreGraphics.framework */; }; 2C90E5691EDDE13A0093D85A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C90E5681EDDE13A0093D85A /* UIKit.framework */; }; @@ -548,7 +538,6 @@ 2CBD0D5A2C8770A40013C089 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD0D592C8770A40013C089 /* UIImageExtension.swift */; }; 2CBF82AE1FC888FC00636459 /* NCPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82AD1FC888FC00636459 /* NCPushNotification.m */; }; 2CBF82B21FCC7DBA00636459 /* CCCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */; }; - 2CC0015324A1F0E900A20167 /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CC0015224A1F0E900A20167 /* NotificationService.m */; }; 2CC0015724A1F0E900A20167 /* NotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1CCA91F02D1A4002FE6A2 /* NCAPIController.m */; }; 2CC0016324A25B7400A20167 /* NCDatabaseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C40281422832EED0000DDFC /* NCDatabaseManager.m */; }; @@ -589,6 +578,8 @@ 2CD5F3242142781A006B71BF /* NCExternalSignalingController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CD5F3232142781A006B71BF /* NCExternalSignalingController.m */; }; 2CD80F482A4304AD00919057 /* OpenConversationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD80F472A4304AD00919057 /* OpenConversationsTableViewController.swift */; }; 2CEDA88C26F492610044552B /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEDA88B26F492610044552B /* NSMutableAttributedString+Extensions.swift */; }; + 2CF338E12CED388B0029CACC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF338E02CED388B0029CACC /* AvatarView.swift */; }; + 2CF338E22CED388B0029CACC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF338E02CED388B0029CACC /* AvatarView.swift */; }; 2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF8AD3D2A0010FB00A4D3E6 /* MessageTranslationViewController.swift */; }; 2CF8AD402A0010FB00A4D3E6 /* MessageTranslationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */; }; 3FCA62550CD1442D28E8A7C6 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */; }; @@ -600,6 +591,7 @@ 847EFC7236336B67A1A89358 /* libPods-BroadcastUploadExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3D305FCD7BF7E727A62F35 /* libPods-BroadcastUploadExtension.a */; }; 8789AE73BFCAA413B43319C0 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 684807120F4439797973DF73 /* libPods-ShareExtension.a */; }; 9993261EDAC77481FF4EF58A /* libPods-NextcloudTalk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F7C31E9D74F550EAF89931B /* libPods-NextcloudTalk.a */; }; + C65D252D2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */; }; DA1AEFC3270F1FA90088E519 /* DateLabelCustom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */; }; DA66582B27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582A27B6992F00B46B11 /* UserProfileTableViewController+AvatarSetup.swift */; }; DA66582D27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66582C27B6A73800B46B11 /* UserProfileTableViewController+DelegateMethods.swift */; }; @@ -700,6 +692,13 @@ 1F1DF83B2C5C17AF00E5EA86 /* TalkActor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TalkActor.swift; sourceTree = ""; }; 1F1DF8402C63C25900E5EA86 /* UnitNCDatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitNCDatabaseManager.swift; sourceTree = ""; }; 1F1DF8422C64006E00E5EA86 /* SignalingParticipant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalingParticipant.swift; sourceTree = ""; }; + 1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AiSummaryViewController.swift; sourceTree = ""; }; + 1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AiSummaryViewController.xib; sourceTree = ""; }; + 1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AiSummaryController.swift; sourceTree = ""; }; + 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAbsence.swift; sourceTree = ""; }; + 1F205C542CEFA01200AAA673 /* OutOfOfficeView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OutOfOfficeView.xib; sourceTree = ""; }; + 1F205C562CEFA01900AAA673 /* OutOfOfficeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfOfficeView.swift; sourceTree = ""; }; + 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarProtocol.swift; sourceTree = ""; }; 1F21A0622C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/InfoPlist.strings"; sourceTree = ""; }; 1F21A0632C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; 1F21A0642C77863500ED8C0C /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -712,9 +711,9 @@ 1F21A06B2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/InfoPlist.strings; sourceTree = ""; }; 1F21A06C2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; 1F21A06D2C77869600ED8C0C /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = ""; }; + 1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 1F24B5A128E0648600654457 /* ReferenceGithubView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGithubView.swift; sourceTree = ""; }; 1F24B5A328E0649200654457 /* ReferenceGithubView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGithubView.xib; sourceTree = ""; }; - 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareConfirmationViewController.swift; sourceTree = ""; }; 1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerExtension.swift; sourceTree = ""; }; 1F35F9022AEEDEE800044BDA /* AutoCompletionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoCompletionTableViewCell.h; sourceTree = ""; }; 1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoCompletionTableViewCell.m; sourceTree = ""; }; @@ -758,10 +757,8 @@ 1F77A6132AB9B161007B6037 /* ScreenCapturer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScreenCapturer.m; sourceTree = ""; }; 1F77A6142AB9B161007B6037 /* ScreenCapturer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScreenCapturer.h; sourceTree = ""; }; 1F77A61C2AB9B301007B6037 /* CapturerEventsDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CapturerEventsDelegate.h; sourceTree = ""; }; - 1F77A61F2AB9D82B007B6037 /* BroadcastUploadExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BroadcastUploadExtension.entitlements; sourceTree = ""; }; 1F77A6202AB9EB06007B6037 /* SocketConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = ""; }; 1F77A6212AB9EB06007B6037 /* SocketConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = ""; }; - 1F77A6232ABA0003007B6037 /* SampleHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = ""; }; 1F77A6252ABA0CD9007B6037 /* NCScreensharingController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCScreensharingController.h; sourceTree = ""; }; 1F77A6262ABA0CD9007B6037 /* NCScreensharingController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCScreensharingController.m; sourceTree = ""; }; 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = ""; }; @@ -806,6 +803,7 @@ 1FBC3BE82B61BD09003909E0 /* TestBaseRealm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBaseRealm.swift; sourceTree = ""; }; 1FC18FBF2CB7EA300058F621 /* AVRoutePickerViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVRoutePickerViewExtension.swift; sourceTree = ""; }; 1FC18FC12CB7EA5C0058F621 /* RPSystemBroadcastPickerViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RPSystemBroadcastPickerViewExtension.swift; sourceTree = ""; }; + 1FC4B3412CCE670400D28138 /* OcsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OcsError.swift; sourceTree = ""; }; 1FCE3D542C9C189D009C68A9 /* NCChatFileControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatFileControllerWrapper.swift; sourceTree = ""; }; 1FCE3D562C9C4D18009C68A9 /* ReferenceGiphyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGiphyView.swift; sourceTree = ""; }; 1FCE3D582C9C4D21009C68A9 /* ReferenceGiphyView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGiphyView.xib; sourceTree = ""; }; @@ -819,16 +817,15 @@ 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = ""; }; 1FDCC3EC29EC7DD400DEB39B /* NextcloudTalk-Bridging-Header-Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NextcloudTalk-Bridging-Header-Extensions.h"; sourceTree = ""; }; 1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = ""; }; - 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundsChangedFlowLayout.swift; sourceTree = ""; }; 1FDDB0E42AFD046600FBAFB7 /* NCUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUtils.swift; sourceTree = ""; }; 1FDDB0E82AFE8F5C00FBAFB7 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 1FDE7C9928DE14A200CB718E /* ReferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferenceView.swift; sourceTree = ""; }; 1FDE7C9B28DE14B000CB718E /* ReferenceView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReferenceView.xib; sourceTree = ""; }; 1FDFC94C2BA50B9100670DF4 /* UIFontExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFontExtension.swift; sourceTree = ""; }; + 1FE029B92CE289A200C0C633 /* MessageSeparatorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSeparatorTableViewCell.swift; sourceTree = ""; }; 1FE0C56B2A0531200083576A /* ReferenceTalkView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceTalkView.xib; sourceTree = ""; }; 1FE0C56D2A0531270083576A /* ReferenceTalkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceTalkView.swift; sourceTree = ""; }; - 1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomInvitationViewCell.swift; sourceTree = ""; }; - 1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomInvitationViewCell.xib; sourceTree = ""; }; + 1FE7DE2F2BB4598F0040EE12 /* InfoLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoLabelTableViewCell.swift; sourceTree = ""; }; 1FE94733293CE55600D6584C /* NCCameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraController.swift; sourceTree = ""; }; 1FEC459B2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReferenceGithubPermalinkView.xib; sourceTree = ""; }; 1FEC459D2A02BCB900A636AA /* ReferenceGithubPermalinkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceGithubPermalinkView.swift; sourceTree = ""; }; @@ -842,11 +839,6 @@ 1FF136142BFB74C3006A6101 /* NCChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatMessage.swift; sourceTree = ""; }; 1FF2FD5B2AB99CCB000C9905 /* BroadcastUploadExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BroadcastUploadExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 1FF2FD5C2AB99CCB000C9905 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; - 1FF2FD612AB99CCB000C9905 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = ""; }; - 1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; - 1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = ""; }; - 1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketConnection.swift; sourceTree = ""; }; 1FF4DA7D2C0237D000C1B952 /* DirectoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryTableViewCell.swift; sourceTree = ""; }; 1FF4DA7F2C023FF300C1B952 /* NCChatFileStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCChatFileStatus.swift; sourceTree = ""; }; 1FF4DA812C025DB900C1B952 /* NCAPISessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAPISessionManager.swift; sourceTree = ""; }; @@ -880,16 +872,7 @@ 2C06BF6320AC64370031EB46 /* DateHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DateHeaderView.xib; sourceTree = ""; }; 2C06BF6520AC647A0031EB46 /* DateHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DateHeaderView.h; sourceTree = ""; }; 2C06BF6620AC647A0031EB46 /* DateHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DateHeaderView.m; sourceTree = ""; }; - 2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoundedNumberView.m; sourceTree = ""; }; - 2C06BF6B20AEB0030031EB46 /* RoundedNumberView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoundedNumberView.h; sourceTree = ""; }; 2C16A82B28E7284D00EDE523 /* NCButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCButton.swift; sourceTree = ""; }; - 2C1ABD7F25769E7C00AEDFB6 /* ShareItemController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareItemController.h; sourceTree = ""; }; - 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareConfirmationCollectionViewCell.m; sourceTree = ""; }; - 2C1ABD8125769E7D00AEDFB6 /* ShareItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareItem.h; sourceTree = ""; }; - 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareItemController.m; sourceTree = ""; }; - 2C1ABD8325769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareConfirmationCollectionViewCell.h; sourceTree = ""; }; - 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareConfirmationCollectionViewCell.xib; sourceTree = ""; }; - 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareItem.m; sourceTree = ""; }; 2C1ABDC4257A7CF000AEDFB6 /* NCContactsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCContactsManager.h; sourceTree = ""; }; 2C1ABDC5257A7CF000AEDFB6 /* NCContactsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCContactsManager.m; sourceTree = ""; }; 2C1ABDCC257E939600AEDFB6 /* NCContact.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCContact.h; sourceTree = ""; }; @@ -906,10 +889,6 @@ 2C2D7A162B8C9C0000642373 /* RoomCreationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomCreationTableViewController.swift; sourceTree = ""; }; 2C2E64231F3462AF00D39CE8 /* NCSignalingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCSignalingMessage.h; sourceTree = ""; }; 2C2E64241F3462AF00D39CE8 /* NCSignalingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCSignalingMessage.m; sourceTree = ""; }; - 2C3195BB24C1F58A0066F221 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; - 2C3195BF24C5E2100066F221 /* ShareTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareTableViewCell.h; sourceTree = ""; }; - 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareTableViewCell.m; sourceTree = ""; }; - 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableViewCell.xib; sourceTree = ""; }; 2C330373255E6EBC00BDB4E4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 2C36A048261487BC0026F04A /* DetailedOptionsSelectorTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailedOptionsSelectorTableViewController.h; sourceTree = ""; }; 2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailedOptionsSelectorTableViewController.m; sourceTree = ""; }; @@ -1003,10 +982,6 @@ 2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SystemMessageTableViewCell.m; sourceTree = ""; }; 2C6085C11FB1063700B36A6E /* NextcloudTalk.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NextcloudTalk.entitlements; sourceTree = ""; }; 2C62AFA324C08845007E460A /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 2C62AFAB24C08845007E460A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2C62AFB524C1A449007E460A /* Share.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Share.storyboard; sourceTree = ""; }; - 2C62AFB724C1A4E6007E460A /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = ""; }; - 2C62AFB824C1A4E6007E460A /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = ""; }; 2C67905128D35BEB00762744 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sl; path = sl.lproj/Localizable.stringsdict; sourceTree = ""; }; 2C69323B2923ECAA00017AD2 /* WSMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WSMessage.h; sourceTree = ""; }; 2C69323C2923ECAA00017AD2 /* WSMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WSMessage.m; sourceTree = ""; }; @@ -1014,6 +989,7 @@ 2C6E74452386D33200AE396C /* ReplyMessageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReplyMessageView.m; sourceTree = ""; }; 2C6E7447238C1A0800AE396C /* QuotedMessageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QuotedMessageView.h; sourceTree = ""; }; 2C6E7448238C1A0800AE396C /* QuotedMessageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuotedMessageView.m; sourceTree = ""; }; + 2C6F0D6B2CDE26B0005E0812 /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = ""; }; 2C7381542106136000CDB8DB /* NCChatTitleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NCChatTitleView.h; sourceTree = ""; }; 2C7381552106136000CDB8DB /* NCChatTitleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NCChatTitleView.m; sourceTree = ""; }; 2C738157210613A200CDB8DB /* NCChatTitleView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCChatTitleView.xib; sourceTree = ""; }; @@ -1040,8 +1016,6 @@ 2C8A2BCD221FEEFE00DE6D2C /* DirectoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryTableViewCell.xib; sourceTree = ""; }; 2C8CDD0421C2EDE8004E2997 /* AvatarBackgroundImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvatarBackgroundImageView.h; sourceTree = ""; }; 2C8CDD0521C2EDE8004E2997 /* AvatarBackgroundImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AvatarBackgroundImageView.m; sourceTree = ""; }; - 2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageSeparatorTableViewCell.h; sourceTree = ""; }; - 2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MessageSeparatorTableViewCell.m; sourceTree = ""; }; 2C90E5631EDDE0FB0093D85A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 2C90E5661EDDE1340093D85A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 2C90E5681EDDE13A0093D85A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; @@ -1144,10 +1118,6 @@ 2CBF82B01FCC7DBA00636459 /* CCCertificate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCCertificate.h; sourceTree = ""; }; 2CBF82B11FCC7DBA00636459 /* CCCertificate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CCCertificate.m; sourceTree = ""; }; 2CC0014F24A1F0E900A20167 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 2CC0015124A1F0E900A20167 /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = ""; }; - 2CC0015224A1F0E900A20167 /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = ""; }; - 2CC0015424A1F0E900A20167 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 2CC0015B24A1F1D700A20167 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = ""; }; 2CC007B220D7AE990096D91F /* ResultMultiSelectionTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ResultMultiSelectionTableViewController.h; sourceTree = ""; }; 2CC007B320D7AE990096D91F /* ResultMultiSelectionTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ResultMultiSelectionTableViewController.m; sourceTree = ""; }; 2CC007C420D90AE50096D91F /* RoomNameTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomNameTableViewCell.xib; sourceTree = ""; }; @@ -1184,6 +1154,7 @@ 2CEA990828B8B5780029216A /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; 2CEDA87E26EF91460044552B /* NextcloudTalk-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NextcloudTalk-Bridging-Header.h"; sourceTree = ""; }; 2CEDA88B26F492610044552B /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = ""; }; + 2CF338E02CED388B0029CACC /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 2CF8AD3D2A0010FB00A4D3E6 /* MessageTranslationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTranslationViewController.swift; sourceTree = ""; }; 2CF8AD3E2A0010FB00A4D3E6 /* MessageTranslationViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MessageTranslationViewController.xib; sourceTree = ""; }; 342600BABD1AD1FCA48B5E59 /* Pods-NextcloudTalkTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalkTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalkTests/Pods-NextcloudTalkTests.debug.xcconfig"; sourceTree = ""; }; @@ -1202,6 +1173,7 @@ 9B81BB7A4920C391CC2CACFD /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A8F95DE6635ABC1E64CA8E4A /* Pods-BroadcastUploadExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BroadcastUploadExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-BroadcastUploadExtension/Pods-BroadcastUploadExtension.release.xcconfig"; sourceTree = ""; }; B7874918820589BF8FD69BED /* Pods-NextcloudTalkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NextcloudTalkTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-NextcloudTalkTests/Pods-NextcloudTalkTests.release.xcconfig"; sourceTree = ""; }; + C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedVoiceMessageRecordingView.swift; sourceTree = ""; }; D6DF51D976DC0F681FF83F7B /* Pods-NotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; D86091EC1125C3057B9A299B /* Pods-NotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; DA1AEFC2270F1FA90088E519 /* DateLabelCustom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateLabelCustom.swift; sourceTree = ""; }; @@ -1214,6 +1186,74 @@ F644A2DC2CE287FA00E2ED81 /* ChatFileUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFileUploader.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 1F2058712CEBE4E200AAA673 /* Exceptions for "NotificationServiceExtension" folder in "NotificationServiceExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + NotificationService.m, + ); + target = 2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */; + }; + 1F20589A2CEBE4E700AAA673 /* Exceptions for "ShareExtension" folder in "NextcloudTalk" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + Share.storyboard, + ); + target = 2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */; + }; + 1F20589B2CEBE4E700AAA673 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 2C62AFA224C08845007E460A /* ShareExtension */; + }; + 1F2058AB2CEBE4EA00AAA673 /* Exceptions for "BroadcastUploadExtension" folder in "NextcloudTalk" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + DarwinNotificationCenter.swift, + ); + target = 2C05747C1EDD9E8E00D9E7F2 /* NextcloudTalk */; + }; + 1F2058AC2CEBE4EA00AAA673 /* Exceptions for "BroadcastUploadExtension" folder in "BroadcastUploadExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 1F20586F2CEBE4E200AAA673 /* NotificationServiceExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 1F2058712CEBE4E200AAA673 /* Exceptions for "NotificationServiceExtension" folder in "NotificationServiceExtension" target */, + ); + path = NotificationServiceExtension; + sourceTree = ""; + }; + 1F2058832CEBE4E700AAA673 /* ShareExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 1F20589A2CEBE4E700AAA673 /* Exceptions for "ShareExtension" folder in "NextcloudTalk" target */, + 1F20589B2CEBE4E700AAA673 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */, + ); + path = ShareExtension; + sourceTree = ""; + }; + 1F2058A32CEBE4EA00AAA673 /* BroadcastUploadExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 1F2058AB2CEBE4EA00AAA673 /* Exceptions for "BroadcastUploadExtension" folder in "NextcloudTalk" target */, + 1F2058AC2CEBE4EA00AAA673 /* Exceptions for "BroadcastUploadExtension" folder in "BroadcastUploadExtension" target */, + ); + path = BroadcastUploadExtension; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 1F6D8C2D2B2E3756004376B8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -1529,26 +1569,12 @@ name = Extensions; sourceTree = ""; }; - 1FF2FD5E2AB99CCB000C9905 /* BroadcastUploadExtension */ = { - isa = PBXGroup; - children = ( - 1F77A61F2AB9D82B007B6037 /* BroadcastUploadExtension.entitlements */, - 1FF2FD7A2AB99E4D000C9905 /* Atomic.swift */, - 1FF2FD792AB99E4D000C9905 /* DarwinNotificationCenter.swift */, - 1F77A6232ABA0003007B6037 /* SampleHandler.swift */, - 1FF2FD7B2AB99E4D000C9905 /* SampleUploader.swift */, - 1FF2FD7D2AB99E4D000C9905 /* SocketConnection.swift */, - 1FF2FD612AB99CCB000C9905 /* Info.plist */, - ); - path = BroadcastUploadExtension; - sourceTree = ""; - }; 2C0574741EDD9E8E00D9E7F2 = { isa = PBXGroup; children = ( - 2CC0015024A1F0E900A20167 /* NotificationServiceExtension */, - 2C62AFA424C08845007E460A /* ShareExtension */, - 1FF2FD5E2AB99CCB000C9905 /* BroadcastUploadExtension */, + 1F20586F2CEBE4E200AAA673 /* NotificationServiceExtension */, + 1F2058832CEBE4E700AAA673 /* ShareExtension */, + 1F2058A32CEBE4EA00AAA673 /* BroadcastUploadExtension */, 1F6D8C312B2E3756004376B8 /* NextcloudTalkTests */, 2C05747E1EDD9E8E00D9E7F2 /* Products */, 2C05749C1EDDA01700D9E7F2 /* ThirdParty */, @@ -1655,23 +1681,28 @@ 2C36A049261487BC0026F04A /* DetailedOptionsSelectorTableViewController.m */, 1F61C76A285F65E1004D74D8 /* SimpleTableViewController.swift */, 1FB6678E28CE381300D29F8D /* SubtitleTableViewCell.swift */, + 1FE7DE2F2BB4598F0040EE12 /* InfoLabelTableViewCell.swift */, 2CC3166D2CC698E1007CBE16 /* TextFieldTableViewCell.swift */, 2CC316712CC79898007CBE16 /* TextViewTableViewCell.swift */, 1F468E7728DCC7310099597B /* EmojiTextField.swift */, 1F371A362A7B921A006CBFB3 /* DatePickerTextField.swift */, + 2C6F0D6B2CDE26B0005E0812 /* BadgeView.swift */, 2C16A82B28E7284D00EDE523 /* NCButton.swift */, 1F11FB7129C07B04001E21E7 /* NCZoomableView.swift */, 1F8995B22970644C00CABA33 /* ColorGenerator.swift */, 1F1C0D8829AFB89900D17C6D /* VLCKitVideoViewController.swift */, 1F1C0D8629AFB88800D17C6D /* VLCKitVideoViewController.xib */, 1F90DA0329E9A28E00E81E3D /* AvatarManager.swift */, - 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */, + 1F205D402CFC6DCF00AAA673 /* AvatarProtocol.swift */, 1FDCC3EF29ECB4CE00DEB39B /* AvatarButton.swift */, + 1FDCC3D329EBF6E700DEB39B /* AvatarImageView.swift */, + 2CF338E02CED388B0029CACC /* AvatarView.swift */, 1F3C41A229EDF05700F58435 /* AvatarEditView.swift */, 1F3C41A429EDF0B800F58435 /* AvatarEditView.xib */, 2CB997C32A052449003C41AC /* EmojiAvatarPickerViewController.swift */, 2CB997C42A052449003C41AC /* EmojiAvatarPickerViewController.xib */, 1F1B0F352BDD8B9C003FD766 /* NCActivityIndicator.swift */, + 1F21A04F2C747FC500ED8C0C /* BlurHashDecode.swift */, ); name = "User Interface"; sourceTree = ""; @@ -1725,10 +1756,6 @@ isa = PBXGroup; children = ( 1FB7B9932BF0DE4A0093CE98 /* Banned Actors */, - 1FE7DE312BB459B10040EE12 /* RoomInvitationViewCell.xib */, - 1FE7DE2F2BB4598F0040EE12 /* RoomInvitationViewCell.swift */, - 2C06BF6B20AEB0030031EB46 /* RoundedNumberView.h */, - 2C06BF6A20AEB0030031EB46 /* RoundedNumberView.m */, 2CA1CCC11F166CC5002FE6A2 /* NCRoom.h */, 2CA1CCC21F166CC5002FE6A2 /* NCRoom.m */, 1FF1360E2BFB4F8C006A6101 /* NCRoom.swift */, @@ -1852,6 +1879,7 @@ 2C4446EB265D25BA00DF1DBC /* NCKeyChainController.m */, 2C444701265D641300DF1DBC /* NCUserDefaults.h */, 2C444702265D641300DF1DBC /* NCUserDefaults.m */, + 1F205C4F2CEF903000AAA673 /* UserAbsence.swift */, ); name = Settings; sourceTree = ""; @@ -1871,34 +1899,11 @@ 2C5BFBE928772A9A00E75118 /* NCUnifiedSearchController.swift */, 1FF4DA952C0327FF00C1B952 /* NCWebImageDownloaderOperation.swift */, 1FF4DAA92C0A114900C1B952 /* OcsResponse.swift */, + 1FC4B3412CCE670400D28138 /* OcsError.swift */, ); name = Network; sourceTree = ""; }; - 2C62AFA424C08845007E460A /* ShareExtension */ = { - isa = PBXGroup; - children = ( - 2C1ABD8325769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.h */, - 2C1ABD8025769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m */, - 2C1ABD8425769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib */, - 2C1ABD8125769E7D00AEDFB6 /* ShareItem.h */, - 2C1ABD8525769E7D00AEDFB6 /* ShareItem.m */, - 2C1ABD7F25769E7C00AEDFB6 /* ShareItemController.h */, - 2C1ABD8225769E7D00AEDFB6 /* ShareItemController.m */, - 2C3195BB24C1F58A0066F221 /* ShareExtension.entitlements */, - 2C62AFAB24C08845007E460A /* Info.plist */, - 2C62AFB524C1A449007E460A /* Share.storyboard */, - 2C62AFB724C1A4E6007E460A /* ShareViewController.h */, - 2C62AFB824C1A4E6007E460A /* ShareViewController.m */, - 2C3195BF24C5E2100066F221 /* ShareTableViewCell.h */, - 2C3195C024C5E2100066F221 /* ShareTableViewCell.m */, - 2C3195C124C5E2100066F221 /* ShareTableViewCell.xib */, - 1F35F8DF2AEEB9DE00044BDA /* ShareConfirmationViewController.swift */, - 1FDDB0D82AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift */, - ); - path = ShareExtension; - sourceTree = ""; - }; 2C6DEAB2243CCC7F00AE8437 /* Chat cells */ = { isa = PBXGroup; children = ( @@ -1910,14 +1915,13 @@ 2C21446D2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift */, 2C04248F2CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift */, 2C4747E52CB6710F002828F2 /* BaseChatTableViewCell+Poll.swift */, + 1FE029B92CE289A200C0C633 /* MessageSeparatorTableViewCell.swift */, 2CC7159220C54D080045C789 /* ChatTableViewCell.h */, 2CC7159320C54D080045C789 /* ChatTableViewCell.m */, 1F35F9022AEEDEE800044BDA /* AutoCompletionTableViewCell.h */, 1F35F9032AEEDF0E00044BDA /* AutoCompletionTableViewCell.m */, 2C604BD7211988A700D34DCD /* SystemMessageTableViewCell.h */, 2C604BD8211988A700D34DCD /* SystemMessageTableViewCell.m */, - 2C8E2A19232174C20022BFC9 /* MessageSeparatorTableViewCell.h */, - 2C8E2A1A232174C20022BFC9 /* MessageSeparatorTableViewCell.m */, 1F0A1D432A5F1FA800A25433 /* SwiftMarkdownObjCBridge.swift */, 1F5683CE2BA7980C0023E151 /* FilePreviewImageView.swift */, ); @@ -1952,6 +1956,9 @@ 1F66B72029FA7089003FB168 /* TypingIndicatorView.xib */, 2C0424992CA33681004772F6 /* AudioPlayerView.swift */, 2C0424962CA335C4004772F6 /* AudioPlayerView.xib */, + C65D252C2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift */, + 1F205C562CEFA01900AAA673 /* OutOfOfficeView.swift */, + 1F205C542CEFA01200AAA673 /* OutOfOfficeView.xib */, ); name = "Chat views"; sourceTree = ""; @@ -2091,17 +2098,6 @@ name = Security; sourceTree = ""; }; - 2CC0015024A1F0E900A20167 /* NotificationServiceExtension */ = { - isa = PBXGroup; - children = ( - 2CC0015B24A1F1D700A20167 /* NotificationServiceExtension.entitlements */, - 2CC0015124A1F0E900A20167 /* NotificationService.h */, - 2CC0015224A1F0E900A20167 /* NotificationService.m */, - 2CC0015424A1F0E900A20167 /* Info.plist */, - ); - path = NotificationServiceExtension; - sourceTree = ""; - }; 2CC1C37F29C0945600C8436B /* DRCellSlideGestureRecognizer */ = { isa = PBXGroup; children = ( @@ -2154,6 +2150,9 @@ 2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */, 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */, F644A2DC2CE287FA00E2ED81 /* ChatFileUploader.swift */, + 1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */, + 1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */, + 1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */, ); name = Chat; sourceTree = ""; @@ -2241,6 +2240,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 1F2058A32CEBE4EA00AAA673 /* BroadcastUploadExtension */, + ); name = BroadcastUploadExtension; packageProductDependencies = ( 1F77A5EE2AB9A41E007B6037 /* SDWebImage */, @@ -2276,6 +2278,9 @@ 2C62AFAD24C08845007E460A /* PBXTargetDependency */, 1FF2FD712AB99CCB000C9905 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 1F2058832CEBE4E700AAA673 /* ShareExtension */, + ); name = NextcloudTalk; packageProductDependencies = ( 2CCCD21C2835088F00F076CE /* OpenSSL */, @@ -2313,6 +2318,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 1F2058832CEBE4E700AAA673 /* ShareExtension */, + ); name = ShareExtension; packageProductDependencies = ( 1F7AE07B29142E6A009F72AD /* NextcloudKit */, @@ -2410,7 +2418,6 @@ }; }; buildConfigurationList = 2C0574781EDD9E8E00D9E7F2 /* Build configuration list for PBXProject "NextcloudTalk" */; - compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( @@ -2461,6 +2468,7 @@ 1FC4B32F2CC057B700D28138 /* XCRemoteSwiftPackageReference "MBProgressHUD" */, 1FC4B3342CC0586A00D28138 /* XCRemoteSwiftPackageReference "libPhoneNumber-iOS" */, ); + preferredProjectObjectVersion = 77; productRefGroup = 2C05747E1EDD9E8E00D9E7F2 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -2511,6 +2519,7 @@ 1FCE3D592C9C4D21009C68A9 /* ReferenceGiphyView.xib in Resources */, 2C7F47AA20289B9600081CC7 /* Localizable.strings in Resources */, 2CB997C62A052449003C41AC /* EmojiAvatarPickerViewController.xib in Resources */, + 1F205C552CEFA01200AAA673 /* OutOfOfficeView.xib in Resources */, 2C0574A51EDDA2E300D9E7F2 /* LoginViewController.xib in Resources */, 1F46CE2B28E05B3C00E7D88E /* ReferenceDefaultView.xib in Resources */, 1F98DF9E28E7485000E05174 /* ReferenceDeckView.xib in Resources */, @@ -2539,19 +2548,17 @@ 2C5BFBFC2891598A00E75118 /* PollResultTableViewCell.xib in Resources */, 1FADECDA2B8227B1007AD94B /* FederationInvitationCell.xib in Resources */, 1FEC459C2A02BCAE00A636AA /* ReferenceGithubPermalinkView.xib in Resources */, + 1F20582C2CEA405700AAA673 /* AiSummaryViewController.xib in Resources */, 1FE0C56C2A0531200083576A /* ReferenceTalkView.xib in Resources */, 1F1B50382B8E070100B0F2F4 /* BaseChatTableViewCell.xib in Resources */, 2C5BFBF828902E3700E75118 /* PollFooterView.xib in Resources */, 1FDE7C9C28DE14B000CB718E /* ReferenceView.xib in Resources */, - 2C444707265E59B500DF1DBC /* ShareConfirmationCollectionViewCell.xib in Resources */, 2CC7158920B837140045C789 /* PlaceholderView.xib in Resources */, 1F1C0D8729AFB88800D17C6D /* VLCKitVideoViewController.xib in Resources */, 2CF8AD402A0010FB00A4D3E6 /* MessageTranslationViewController.xib in Resources */, 1FE7DE362BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */, 2C477C1628B79D980044DEB4 /* Localizable.stringsdict in Resources */, 2CB6ACEC2641954700D3D641 /* MapViewController.xib in Resources */, - 1FE7DE322BB459B10040EE12 /* RoomInvitationViewCell.xib in Resources */, - 2C4CDCD226A84E550023F403 /* ShareTableViewCell.xib in Resources */, 1FEC45A52A02F92B00A636AA /* GithubPermalinkViewController.xib in Resources */, 2C7A12432017872600864818 /* AddParticipantsTableViewController.xib in Resources */, 1F3C41A129EDAC8800F58435 /* RoomAvatarInfoTableViewController.xib in Resources */, @@ -2565,12 +2572,9 @@ files = ( 1F35F8E42AEEBBE500044BDA /* NCChatTitleView.xib in Resources */, 2C3195BC24C599130066F221 /* PlaceholderView.xib in Resources */, - 2C1ABD8825769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.xib in Resources */, - 2C3195C324C5E2100066F221 /* ShareTableViewCell.xib in Resources */, 2C3195BE24C5A7410066F221 /* Images.xcassets in Resources */, 1F59446625B8EDF5002AD65F /* Localizable.strings in Resources */, 2CB6ACEE2641954700D3D641 /* MapViewController.xib in Resources */, - 2C62AFB624C1A449007E460A /* Share.storyboard in Resources */, 1FE7DE342BBC8FA10040EE12 /* PrivacyInfo.xcprivacy in Resources */, 1F35F8F22AEEC25E00044BDA /* TypingIndicatorView.xib in Resources */, ); @@ -2687,15 +2691,12 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-resources.sh", - "${PODS_ROOT}/DateTools/DateTools/DateTools/DateTools.bundle", - "${PODS_ROOT}/MaterialComponents/components/ActivityIndicator/src/MaterialActivityIndicator.bundle", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DateTools.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialActivityIndicator.bundle", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2707,13 +2708,12 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/MobileVLCKit/MobileVLCKit.framework/MobileVLCKit", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MobileVLCKit.framework", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NextcloudTalk/Pods-NextcloudTalk-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2815,9 +2815,9 @@ 1F77A5EC2AB9A405007B6037 /* NCChatBlock.m in Sources */, 1F77A5F42AB9A4B2007B6037 /* ABContact.m in Sources */, 1F77A6012AB9A51D007B6037 /* NCNotificationAction.swift in Sources */, - 1F77A6242ABA0003007B6037 /* SampleHandler.swift in Sources */, 1FB7B9902BF0CDF80093CE98 /* BannedActor.swift in Sources */, 1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */, + 1FC4B3452CCE671800D28138 /* OcsError.swift in Sources */, 1FF4DA832C025DBF00C1B952 /* NCAPISessionManager.swift in Sources */, 1FDFC9502BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 1F1DF8462C64006E00E5EA86 /* SignalingParticipant.swift in Sources */, @@ -2836,9 +2836,9 @@ 1FF136132BFB6FCD006A6101 /* RLMSupport.swift in Sources */, 1F77A5ED2AB9A408007B6037 /* NCChatMessage.m in Sources */, 1F77A5EB2AB9A3EE007B6037 /* BGTaskHelper.swift in Sources */, + 1F205C532CEF91C500AAA673 /* UserAbsence.swift in Sources */, 1FF136182BFB74D0006A6101 /* NCChatMessage.swift in Sources */, 1F77A5FC2AB9A4ED007B6037 /* NCRoom.m in Sources */, - 1F77A62E2ABAFCC0007B6037 /* DarwinNotificationCenter.swift in Sources */, 1F77A60D2AB9A5CC007B6037 /* NCPoll.m in Sources */, 1F77A6032AB9A56D007B6037 /* NotificationCenterNotifications.m in Sources */, 1F77A60A2AB9A5AE007B6037 /* NCUser.m in Sources */, @@ -2848,16 +2848,13 @@ 1F1B504A2B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */, 2C6955122B0CE1A10070F6E1 /* NCUtils.swift in Sources */, 1FF4DA8D2C0264B100C1B952 /* NCPushProxySessionManager.swift in Sources */, - 1FF2FD802AB99E4D000C9905 /* SampleUploader.swift in Sources */, 1F77A5F52AB9A4B9007B6037 /* NCAPIController.m in Sources */, 1F77A5F92AB9A4D9007B6037 /* NCMessageFileParameter.m in Sources */, 1FF2FD852AB99F51000C9905 /* NCUserStatus.m in Sources */, 1F8AAC3A2C519577004DA20A /* TurnServer.swift in Sources */, 1FB78E202B6ADBB600B0D69D /* NCAPIControllerExtensions.swift in Sources */, 1FF136122BFB4F8C006A6101 /* NCRoom.swift in Sources */, - 1FF2FD7F2AB99E4D000C9905 /* Atomic.swift in Sources */, 1F77A5F72AB9A4C5007B6037 /* NCContact.m in Sources */, - 1FF2FD822AB99E4D000C9905 /* SocketConnection.swift in Sources */, 1F77A6082AB9A58D007B6037 /* NCRoomParticipants.m in Sources */, 1FF4DAAD2C0A114900C1B952 /* OcsResponse.swift in Sources */, 1FF2FD862AB99F5B000C9905 /* NCDatabaseManager.m in Sources */, @@ -2877,6 +2874,7 @@ 1FEDE3C6257D439500853F79 /* NCChatFileController.m in Sources */, 1FEDE3CE257D43AB00853F79 /* NCMessageFileParameter.m in Sources */, 2C4446DD2658158000DF1DBC /* NCChatBlock.m in Sources */, + 1F205D412CFC6DD300AAA673 /* AvatarProtocol.swift in Sources */, 1FDCC3F029ECB4CE00DEB39B /* AvatarButton.swift in Sources */, 2C06BF6720AC647A0031EB46 /* DateHeaderView.m in Sources */, 2CB6ACDA2641483800D3D641 /* NCMessageLocationParameter.m in Sources */, @@ -2887,6 +2885,7 @@ 2C0574851EDD9E8E00D9E7F2 /* AppDelegate.m in Sources */, 2C4987BD21E640E20060AC27 /* CallKitManager.m in Sources */, 1F1B0F4C2BE18FF3003FD766 /* CustomPresentableNavigationController.swift in Sources */, + 1F21A0502C747FC500ED8C0C /* BlurHashDecode.swift in Sources */, 2C4446F3265D51A600DF1DBC /* NCPushNotificationsUtils.m in Sources */, 2C0424902CA32D45004772F6 /* BaseChatTableViewCell+Audio.swift in Sources */, 2C1ABDE5257F883400AEDFB6 /* ABContact.m in Sources */, @@ -2929,6 +2928,7 @@ 1FB7B9952BF0DF1C0093CE98 /* BannedActorTableViewController.swift in Sources */, 2C1ABDC6257A7CF000AEDFB6 /* NCContactsManager.m in Sources */, 2C5BFBEA28772A9A00E75118 /* NCUnifiedSearchController.swift in Sources */, + 1F205BA02CEE1B8F00AAA673 /* AiSummaryController.swift in Sources */, 1F4DD3EB2571C688007DC98E /* EmojiUtils.swift in Sources */, 2C4D7D731F309DA500FF4A0D /* RTCSessionDescription+JSON.m in Sources */, 2CB3041C2264775E0053078A /* SLKTextView+SLKAdditions.m in Sources */, @@ -2938,7 +2938,6 @@ 2C5BFBFB2891598A00E75118 /* PollResultTableViewCell.swift in Sources */, 1FA20C8A284001D80062B4F3 /* DebounceWebView.swift in Sources */, 1F1B0F322BDC57E3003FD766 /* UIPageViewControllerExtension.swift in Sources */, - 2C4CDCD126A84E500023F403 /* ShareTableViewCell.m in Sources */, 2C8CDD0621C2EDE8004E2997 /* AvatarBackgroundImageView.m in Sources */, 2C9B0B9C217F756B00A4752C /* NCNotification.m in Sources */, 2C2D7A172B8C9C0000642373 /* RoomCreationTableViewController.swift in Sources */, @@ -2947,13 +2946,12 @@ 2C2145682BF6B8E900470C0C /* NewRoomTableViewController.swift in Sources */, 1F1B503E2B8FB12100B0F2F4 /* BaseChatTableViewCell+Message.swift in Sources */, DA66583127B6B24E00B46B11 /* UserProfileTableViewController+Utils.swift in Sources */, + 1F205C572CEFA01900AAA673 /* OutOfOfficeView.swift in Sources */, 1F1B0F422BE047CE003FD766 /* UIViewController+Transitions.swift in Sources */, 1F90EFBC25FE39F800F3FA55 /* NCIntentController.m in Sources */, - 2C1ABD9925769F7500AEDFB6 /* ShareItem.m in Sources */, 2C2E64251F3462AF00D39CE8 /* NCSignalingMessage.m in Sources */, 2CA1554B208F2E5700CE8EF0 /* NCMessageTextView.m in Sources */, 2C4D7D721F309DA500FF4A0D /* RTCIceCandidate+JSON.m in Sources */, - 1F77A62F2ABAFCEB007B6037 /* DarwinNotificationCenter.swift in Sources */, 2C9E6CCE1F6F34F000399B7A /* ARDSDPUtils.m in Sources */, 2C06330F2046CC8B0043481A /* NCUserInterfaceController.m in Sources */, 2CB304222264775E0053078A /* UIView+SLKAdditions.m in Sources */, @@ -2997,12 +2995,10 @@ 1F66B72C29FA9414003FB168 /* SLKDefaultTypingIndicatorView.m in Sources */, 1FC18FC22CB7EA5C0058F621 /* RPSystemBroadcastPickerViewExtension.swift in Sources */, 1F46CE2928E05B3200E7D88E /* ReferenceDefaultView.swift in Sources */, - 2C444706265E59B100DF1DBC /* ShareConfirmationCollectionViewCell.m in Sources */, 1FCE3D552C9C189D009C68A9 /* NCChatFileControllerWrapper.swift in Sources */, 2C78EF991F80F81E008AFA74 /* NCSignalingController.m in Sources */, 1FDB47F82C9C7E3F00D6F423 /* NCDatabaseManager.swift in Sources */, 2CB304202264775E0053078A /* UIResponder+SLKAdditions.m in Sources */, - 2C8E2A1B232174C20022BFC9 /* MessageSeparatorTableViewCell.m in Sources */, 1FAB2EEE2AD1BC1B001214EB /* UIControlExtensions.swift in Sources */, 1F7625E52901B0DB00834869 /* CallsFromOldAccountViewController.swift in Sources */, 2CB3041E2264775E0053078A /* SLKTextViewController.m in Sources */, @@ -3015,6 +3011,7 @@ 1F8D8BE82C35D7C800A6BF2B /* CallViewController.swift in Sources */, 2CA1CCCD1F181741002FE6A2 /* NCUser.m in Sources */, 1F77A6162AB9B161007B6037 /* ScreenCaptureController.m in Sources */, + C65D252D2C7581A200157A89 /* ExpandedVoiceMessageRecordingView.swift in Sources */, 2CF8AD3F2A0010FB00A4D3E6 /* MessageTranslationViewController.swift in Sources */, 2C21446E2BB5B54D005A6537 /* BaseChatTableViewCell+Location.swift in Sources */, 2C4230F72B207AB00013E1FA /* ContextChatViewController.swift in Sources */, @@ -3024,6 +3021,7 @@ 1F1DF8432C64006E00E5EA86 /* SignalingParticipant.swift in Sources */, 2CC1FF4428147F11009F7288 /* RoomSharedItemsTableViewController.swift in Sources */, 2CC1C38629C0945700C8436B /* DRCellSlideGestureRecognizer.m in Sources */, + 1F205C502CEF903000AAA673 /* UserAbsence.swift in Sources */, 1FF4DA9B2C032AAC00C1B952 /* RoomTableViewCell.swift in Sources */, 1FB52E762842C75E00AC741B /* QRCodeLoginController.swift in Sources */, 1F5A24332ADA77DA009939FE /* InputbarViewController.swift in Sources */, @@ -3033,15 +3031,13 @@ 1F371A372A7B921A006CBFB3 /* DatePickerTextField.swift in Sources */, 1F1B0F482BE047CE003FD766 /* CustomPresentable.swift in Sources */, 1FE0C56E2A0531270083576A /* ReferenceTalkView.swift in Sources */, - 1FE7DE302BB4598F0040EE12 /* RoomInvitationViewCell.swift in Sources */, + 1FE7DE302BB4598F0040EE12 /* InfoLabelTableViewCell.swift in Sources */, 1F77A6222AB9EB06007B6037 /* SocketConnection.m in Sources */, 1FF136152BFB74C3006A6101 /* NCChatMessage.swift in Sources */, 1FDB47F62C9C71CE00D6F423 /* TalkAccount.swift in Sources */, 2CC1C38829C0945700C8436B /* DRCellSlideActionView.m in Sources */, 1FA732FC2966CBB7003D2103 /* CallFlowLayout.swift in Sources */, 2C78EF951F7E70EB008AFA74 /* NCPeerConnection.m in Sources */, - 2C06BF6C20AEB0030031EB46 /* RoundedNumberView.m in Sources */, - 1FDDB0DB2AF440E100FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */, DA1AEFC3270F1FA90088E519 /* DateLabelCustom.swift in Sources */, 2C6E74462386D33200AE396C /* ReplyMessageView.m in Sources */, DA8801A227A2DA00009EF248 /* UserProfileTableViewController.swift in Sources */, @@ -3077,7 +3073,7 @@ 1F66B71F29FA703B003FB168 /* TypingIndicatorView.swift in Sources */, 1F35F9042AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */, 2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */, - 2C4CDCD026A84AEA0023F403 /* ShareViewController.m in Sources */, + 1F20582A2CEA404F00AAA673 /* AiSummaryViewController.swift in Sources */, 1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */, 1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */, 1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */, @@ -3093,26 +3089,28 @@ 1FF4DA912C02677C00C1B952 /* NCImageSessionManager.swift in Sources */, 1F24B5A228E0648600654457 /* ReferenceGithubView.swift in Sources */, 2C4D7D691F2F7DBC00FF4A0D /* ARDSettingsModel.m in Sources */, + 2C6F0D6C2CDE26B0005E0812 /* BadgeView.swift in Sources */, 1FF4DA8C2C0263A200C1B952 /* NCPushProxySessionManager.swift in Sources */, 2CB6ACBC26385A3800D3D641 /* ShareLocationViewController.m in Sources */, 2C04249B2CA33681004772F6 /* AudioPlayerView.swift in Sources */, 1F1B0F462BE047CE003FD766 /* InteractionControlling.swift in Sources */, F644A2DD2CE287FA00E2ED81 /* ChatFileUploader.swift in Sources */, + 2CF338E12CED388B0029CACC /* AvatarView.swift in Sources */, 1FDCC3D429EBF6E700DEB39B /* AvatarImageView.swift in Sources */, 1FB78E262B6AE5A600B0D69D /* FederationInvitation.swift in Sources */, 1FDFC94D2BA50B9100670DF4 /* UIFontExtension.swift in Sources */, 1F468E7828DCC7310099597B /* EmojiTextField.swift in Sources */, 80832B762A822E5100195A97 /* UserStatusSwiftUIView.swift in Sources */, + 1FC4B3422CCE670400D28138 /* OcsError.swift in Sources */, 1FF4DA822C025DB900C1B952 /* NCAPISessionManager.swift in Sources */, 1F1DF83C2C5C17AF00E5EA86 /* TalkActor.swift in Sources */, 2C4747E62CB6711F002828F2 /* BaseChatTableViewCell+Poll.swift in Sources */, - 2C444708265E59BC00DF1DBC /* ShareItemController.m in Sources */, + 1FE029BA2CE289A400C0C633 /* MessageSeparatorTableViewCell.swift in Sources */, 2CA1CC951F014EF9002FE6A2 /* NCSettingsController.m in Sources */, 2C440D1120EA4A770005F9BB /* RoomInfoTableViewController.m in Sources */, 1FAB2E832AC9EC3F001214EB /* BaseChatViewController.swift in Sources */, 2C4D7D631F2F7C2C00FF4A0D /* ARDCaptureController.m in Sources */, 2C4D7D6A1F2F7DBC00FF4A0D /* ARDSettingsStore.m in Sources */, - 1F35F8E02AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */, 1FF4DAA02C03351E00C1B952 /* RoomNameTableViewCell.swift in Sources */, 2C9200C32474262C0050084F /* UIBarButtonItem+Badge.m in Sources */, 1FD6F83E2B87B712004048AB /* NCUserStatusExtensions.swift in Sources */, @@ -3129,13 +3127,13 @@ 1F4DD3ED2571C688007DC98E /* EmojiUtils.swift in Sources */, 1F35F8EB2AEEBC1100044BDA /* UIResponder+SLKAdditions.m in Sources */, 2C62B02424C1BDCF007E460A /* NCAppBranding.m in Sources */, - 2C1ABD8625769E7D00AEDFB6 /* ShareConfirmationCollectionViewCell.m in Sources */, 1F35F90B2AEEE76C00044BDA /* ReplyMessageView.m in Sources */, 1F1C999E2909846400EACF02 /* BGTaskHelper.swift in Sources */, 1F35F8F12AEEC25B00044BDA /* TypingIndicatorView.swift in Sources */, 2C62AFFD24C1BDA5007E460A /* NCMessageParameter.m in Sources */, 1F35F8EC2AEEBC1400044BDA /* UIScrollView+SLKAdditions.m in Sources */, 1FF136172BFB74CF006A6101 /* NCChatMessage.swift in Sources */, + 2CF338E22CED388B0029CACC /* AvatarView.swift in Sources */, 1FF4DA8A2C0262BB00C1B952 /* NCBaseSessionManager.swift in Sources */, 2C62B00C24C1BDC1007E460A /* NCNotification.m in Sources */, 1F8AAC3E2C519689004DA20A /* StunServer.swift in Sources */, @@ -3149,7 +3147,6 @@ 2CB6ACDC2641483800D3D641 /* NCMessageLocationParameter.m in Sources */, 1F35F8E82AEEBC0800044BDA /* SLKTextView+SLKAdditions.m in Sources */, 1F35F9052AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */, - 2C1ABD8925769E7D00AEDFB6 /* ShareItem.m in Sources */, 1F35F90A2AEEE76A00044BDA /* QuotedMessageView.m in Sources */, 2C62B02E24C1BDD7007E460A /* PlaceholderView.m in Sources */, 2C62B01024C1BDC5007E460A /* NCRoom.m in Sources */, @@ -3164,8 +3161,8 @@ 2C62B01C24C1BDC9007E460A /* CCCertificate.m in Sources */, 2C4446F9265D5A0700DF1DBC /* NotificationCenterNotifications.m in Sources */, 1F35F8ED2AEEBC1600044BDA /* UIView+SLKAdditions.m in Sources */, - 2C1ABD8725769E7D00AEDFB6 /* ShareItemController.m in Sources */, 1FDFC94F2BA50B9100670DF4 /* UIFontExtension.swift in Sources */, + 1F205D442CFC70AD00AAA673 /* AvatarProtocol.swift in Sources */, 1F35F8EA2AEEBC0E00044BDA /* SLKTextViewController.m in Sources */, 2C6955132B0CE1A20070F6E1 /* NCUtils.swift in Sources */, 1F8AAC342C518B8A004DA20A /* SignalingSettings.swift in Sources */, @@ -3177,11 +3174,9 @@ 2C4446FE265D5DFA00DF1DBC /* ABContact.m in Sources */, 1F35F8EF2AEEBC1A00044BDA /* SLKDefaultReplyView.m in Sources */, 1F35F8E22AEEBAF900044BDA /* InputbarViewController.swift in Sources */, - 1F35F8E12AEEB9DE00044BDA /* ShareConfirmationViewController.swift in Sources */, 1F90EFBE25FE39F800F3FA55 /* NCIntentController.m in Sources */, 1FF4DA842C025DC000C1B952 /* NCAPISessionManager.swift in Sources */, 1F1DF8452C64006E00E5EA86 /* SignalingParticipant.swift in Sources */, - 2C3195C224C5E2100066F221 /* ShareTableViewCell.m in Sources */, 2CC1FF4A2818395F009F7288 /* NCDeckCardParameter.m in Sources */, 1FF136112BFB4F8C006A6101 /* NCRoom.swift in Sources */, 2C4446DA265814D100DF1DBC /* ServerCapabilities.m in Sources */, @@ -3189,7 +3184,7 @@ 2C444705265D641300DF1DBC /* NCUserDefaults.m in Sources */, 1FF4DA8E2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */, 2C4446F5265D583200DF1DBC /* NCKeyChainController.m in Sources */, - 1FDDB0D92AF440DD00FBAFB7 /* BoundsChangedFlowLayout.swift in Sources */, + 1FC4B3442CCE671800D28138 /* OcsError.swift in Sources */, 2C62AFFA24C1BDA5007E460A /* NCChatMessage.m in Sources */, 1FC940BA2A5F21FD00FFFADE /* SwiftMarkdownObjCBridge.swift in Sources */, 2C4446D52658147900DF1DBC /* TalkAccount.m in Sources */, @@ -3198,11 +3193,11 @@ 1F35F8F02AEEBC1D00044BDA /* SLKDefaultTypingIndicatorView.m in Sources */, 1F35F8FC2AEEDBC600044BDA /* ChatViewControllerExtension.swift in Sources */, 1FEDE3D0257D43AB00853F79 /* NCMessageFileParameter.m in Sources */, - 2C62AFB924C1A4E6007E460A /* ShareViewController.m in Sources */, 1F8AAC392C519577004DA20A /* TurnServer.swift in Sources */, 1F35F8F32AEEC29A00044BDA /* AvatarButton.swift in Sources */, 2C4446DF2658158000DF1DBC /* NCChatBlock.m in Sources */, 1FF4DAAC2C0A114900C1B952 /* OcsResponse.swift in Sources */, + 1F205C522CEF91C500AAA673 /* UserAbsence.swift in Sources */, 1F1B504C2B90CF0C00B0F2F4 /* FederatedCapabilities.m in Sources */, 1FB78E282B6AE8C900B0D69D /* FederationInvitation.swift in Sources */, 2CC32E9A27F5DADB00BB8C39 /* NCChatReaction.m in Sources */, @@ -3221,7 +3216,6 @@ 2CC001DC24A37AD400A20167 /* NCAppBranding.m in Sources */, 2C4446D42658147900DF1DBC /* TalkAccount.m in Sources */, 1FDCC3E329EC787400DEB39B /* AvatarManager.swift in Sources */, - 2CC0015324A1F0E900A20167 /* NotificationService.m in Sources */, 1FF4DA852C025DC000C1B952 /* NCAPISessionManager.swift in Sources */, 1FEDE3CF257D43AB00853F79 /* NCMessageFileParameter.m in Sources */, 1FB78E222B6ADBB700B0D69D /* NCAPIControllerExtensions.swift in Sources */, @@ -3261,6 +3255,7 @@ 1F4DD3EC2571C688007DC98E /* EmojiUtils.swift in Sources */, 2C4446ED265D25BA00DF1DBC /* NCKeyChainController.m in Sources */, 1F1B50482B90CF0800B0F2F4 /* TalkCapabilities.m in Sources */, + 1FC4B3432CCE671800D28138 /* OcsError.swift in Sources */, 2CC0016724A25BE100A20167 /* NCChatMessage.m in Sources */, 1FF4DA8F2C0264B200C1B952 /* NCPushProxySessionManager.swift in Sources */, 2CC0016324A25B7400A20167 /* NCDatabaseManager.m in Sources */, @@ -3268,6 +3263,7 @@ 2CC0016924A25C3400A20167 /* NCMessageParameter.m in Sources */, 1FB78E292B6AE8CA00B0D69D /* FederationInvitation.swift in Sources */, 2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */, + 1F205C512CEF91C500AAA673 /* UserAbsence.swift in Sources */, 2CC001B724A37A9A00A20167 /* NCUser.m in Sources */, 2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */, 2CC32E9927F5DADA00BB8C39 /* NCChatReaction.m in Sources */, @@ -3763,7 +3759,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; @@ -3787,7 +3783,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3809,7 +3805,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; @@ -3833,7 +3829,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3864,7 +3860,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; @@ -3920,7 +3916,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/UICKeyChainStore\"", "\"${PODS_ROOT}/Realm/core\"", ); - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.ShareExtension; @@ -3957,7 +3953,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; @@ -4013,7 +4009,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/UICKeyChainStore\"", "\"${PODS_ROOT}/Realm/core\"", ); - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.ShareExtension; PRODUCT_NAME = ShareExtension; @@ -4048,7 +4044,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4085,7 +4081,7 @@ "@executable_path/../../Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.NotificationServiceExtension; @@ -4121,7 +4117,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution: Nextcloud GmbH (NKUJUXUJ3B)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4158,7 +4154,7 @@ "@executable_path/../../Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; - MARKETING_VERSION = 20.0.1; + MARKETING_VERSION = 20.1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.nextcloud.Talk.NotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/NextcloudTalk/AddParticipantsTableViewController.m b/NextcloudTalk/AddParticipantsTableViewController.m index ba98a27b7..bb1f19dba 100644 --- a/NextcloudTalk/AddParticipantsTableViewController.m +++ b/NextcloudTalk/AddParticipantsTableViewController.m @@ -429,7 +429,12 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } cell.labelTitle.text = participant.name; - [cell.contactImage setActorAvatarForId:participant.userId withType:participant.source withDisplayName:participant.name withRoomToken:_room.token]; + + TalkAccount *account = self->_room.account; + + if (account) { + [cell.contactImage setActorAvatarForId:participant.userId withType:participant.source withDisplayName:participant.name withRoomToken:_room.token using:account]; + } UIImage *selectionImage = [UIImage systemImageNamed:@"circle"]; UIColor *selectionImageColor = [UIColor tertiaryLabelColor]; diff --git a/NextcloudTalk/AiSummaryController.swift b/NextcloudTalk/AiSummaryController.swift new file mode 100644 index 000000000..5d5ecfef4 --- /dev/null +++ b/NextcloudTalk/AiSummaryController.swift @@ -0,0 +1,46 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +public typealias RoomInternalIdString = String + +public class AiSummaryTask { + public var taskIds = [Int]() + public var outputs = [String]() +} + +public class AiSummaryController { + + public static let shared = AiSummaryController() + + public var generateSummaryTasks = [RoomInternalIdString: AiSummaryTask]() + + public func addSummaryTaskId(forRoomInternalId internalId: String, withTaskId taskId: Int) { + let task = generateSummaryTasks[internalId, default: AiSummaryTask()] + + task.taskIds.append(taskId) + generateSummaryTasks[internalId] = task + } + + public func markSummaryTaskAsDone(forRoomInternalId internalId: String, withTaskId taskId: Int, withOutput output: String) { + guard let task = generateSummaryTasks[internalId] else { return } + + task.taskIds.removeAll(where: { $0 == taskId }) + task.outputs.append(output) + } + + @discardableResult + public func finalizeSummaryTask(forRoomInternalId internalId: String) -> [String] { + let result = generateSummaryTasks[internalId]?.outputs ?? [] + generateSummaryTasks.removeValue(forKey: internalId) + + return result + } + + public func getSummaryTaskIds(forRoomInternalId internalId: String) -> [Int] { + return generateSummaryTasks[internalId]?.taskIds ?? [] + } +} diff --git a/NextcloudTalk/AiSummaryViewController.swift b/NextcloudTalk/AiSummaryViewController.swift new file mode 100644 index 000000000..dc9f0d258 --- /dev/null +++ b/NextcloudTalk/AiSummaryViewController.swift @@ -0,0 +1,73 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import UIKit + +class AiSummaryViewController: UIViewController { + + private var summaryText: String = "" + + @IBOutlet weak var warningView: UIView! + @IBOutlet weak var warningTextLabel: UILabel! + @IBOutlet weak var saveToNoteToSelf: NCButton! + @IBOutlet weak var summaryTextView: UITextView! + + init(summaryText: String) { + self.summaryText = summaryText + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override func viewDidLoad() { + super.viewDidLoad() + + NCAppBranding.styleViewController(self) + + let barButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) + barButtonItem.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { [unowned self] _ in + self.dismiss(animated: true) + }) + self.navigationItem.rightBarButtonItems = [barButtonItem] + + saveToNoteToSelf.setTitle(NSLocalizedString("Save to 'Note to self'", comment: ""), for: .normal) + saveToNoteToSelf.setButtonStyle(style: .primary) + + warningView.layer.cornerRadius = 8 + warningView.layer.borderColor = NCAppBranding.placeholderColor().cgColor + warningView.layer.borderWidth = 1 + warningView.layer.masksToBounds = true + + warningTextLabel.text = NSLocalizedString("This summary is AI generated and may contain mistakes.", comment: "") + + summaryTextView.text = self.summaryText + } + + @IBAction func saveToNoteToSelfPressed(_ sender: Any) { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + + self.saveToNoteToSelf.setButtonEnabled(enabled: false) + + NCAPIController.sharedInstance().getNoteToSelfRoom(forAccount: activeAccount) { roomDict, error in + if error == nil, let room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) { + + NCAPIController.sharedInstance().sendChatMessage(self.summaryTextView.text, toRoom: room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: activeAccount) { error in + if error == nil { + NotificationPresenter.shared().present(text: NSLocalizedString("Added note to self", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success) + } else { + self.saveToNoteToSelf.setButtonEnabled(enabled: true) + NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding note", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) + } + } + } else { + self.saveToNoteToSelf.setButtonEnabled(enabled: true) + NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding note", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) + } + } + } +} diff --git a/NextcloudTalk/AiSummaryViewController.xib b/NextcloudTalk/AiSummaryViewController.xib new file mode 100644 index 000000000..850570a95 --- /dev/null +++ b/NextcloudTalk/AiSummaryViewController.xib @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut lLorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NextcloudTalk/AppDelegate.m b/NextcloudTalk/AppDelegate.m index 74556aa5e..1ce407a21 100644 --- a/NextcloudTalk/AppDelegate.m +++ b/NextcloudTalk/AppDelegate.m @@ -72,7 +72,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [NCUtils removeOldLogfiles]; }); - [NCUtils log:[NSString stringWithFormat:@"Starting %@, version %@", NSBundle.mainBundle.bundleIdentifier, [NCAppBranding getAppVersionString]]]; + UIDevice *currentDevice = [UIDevice currentDevice]; + [NCUtils log:[NSString stringWithFormat:@"Starting %@, version %@, %@ %@, model %@", NSBundle.mainBundle.bundleIdentifier, [NCAppBranding getAppVersionString], currentDevice.systemName, currentDevice.systemVersion, currentDevice.model]]; //Init rooms manager to start receiving NSNotificationCenter notifications [NCRoomsManager sharedInstance]; diff --git a/NextcloudTalk/AvatarButton.swift b/NextcloudTalk/AvatarButton.swift index 3e768e241..b6ab5ea84 100644 --- a/NextcloudTalk/AvatarButton.swift +++ b/NextcloudTalk/AvatarButton.swift @@ -6,7 +6,7 @@ import UIKit import SDWebImage -@objcMembers class AvatarButton: UIButton { +@objcMembers class AvatarButton: UIButton, AvatarProtocol { private var currentRequest: SDWebImageCombinedOperation? @@ -63,15 +63,11 @@ import SDWebImage // MARK: - User avatars - public func setActorAvatar(forMessage message: NCChatMessage) { - self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token) + public func setActorAvatar(forMessage message: NCChatMessage, withAccount account: TalkAccount) { + self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token, using: account) } - public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?) { - self.setActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, using: nil) - } - - public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount?) { + public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount) { self.cancelCurrentRequest() self.currentRequest = AvatarManager.shared.getActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, withStyle: self.traitCollection.userInterfaceStyle, usingAccount: account) { image in diff --git a/NextcloudTalk/AvatarImageView.swift b/NextcloudTalk/AvatarImageView.swift index 1a6f81eb0..8cd77809b 100644 --- a/NextcloudTalk/AvatarImageView.swift +++ b/NextcloudTalk/AvatarImageView.swift @@ -6,7 +6,7 @@ import UIKit import SDWebImage -@objcMembers class AvatarImageView: UIImageView { +@objcMembers class AvatarImageView: UIImageView, AvatarProtocol { private var currentRequest: SDWebImageCombinedOperation? @@ -60,15 +60,11 @@ import SDWebImage // MARK: - User avatars - public func setActorAvatar(forMessage message: NCChatMessage) { - self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token) + public func setActorAvatar(forMessage message: NCChatMessage, withAccount account: TalkAccount) { + self.setActorAvatar(forId: message.actorId, withType: message.actorType, withDisplayName: message.actorDisplayName, withRoomToken: message.token, using: account) } - public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?) { - self.setActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, using: nil) - } - - public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount?) { + public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount) { self.cancelCurrentRequest() self.currentRequest = AvatarManager.shared.getActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, withStyle: self.traitCollection.userInterfaceStyle, usingAccount: account) { image in diff --git a/NextcloudTalk/AvatarManager.swift b/NextcloudTalk/AvatarManager.swift index 3bca14b5e..1fefb4185 100644 --- a/NextcloudTalk/AvatarManager.swift +++ b/NextcloudTalk/AvatarManager.swift @@ -15,7 +15,7 @@ import SDWebImage // MARK: - Conversation avatars public func getAvatar(for room: NCRoom, with style: UIUserInterfaceStyle, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { - if room.accountId != nil, NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationAvatars, forAccountId: room.accountId) { + if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationAvatars, forAccountId: room.accountId) { // Server supports conversation avatars -> try to get the avatar using this API return NCAPIController.sharedInstance().getAvatarFor(room, with: style) { image, _ in @@ -50,7 +50,7 @@ import SDWebImage } else { switch room.type { case .oneToOne: - let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: room.accountId) + guard let account = room.account else { return nil } return self.getUserAvatar(forId: room.name, withStyle: style, usingAccount: account, completionBlock: completionBlock) case .formerOneToOne: completionBlock(UIImage(named: "user-avatar", in: nil, compatibleWith: traitCollection)) @@ -71,27 +71,25 @@ import SDWebImage // MARK: - Actor avatars // swiftlint:disable:next function_parameter_count - public func getActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { + public func getActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { if let actorId { if actorType == "bots" { return getBotsAvatar(forId: actorId, withStyle: style, completionBlock: completionBlock) - } else if actorType == "guests" { - return getGuestsAvatar(withDisplayName: actorDisplayName ?? "", completionBlock: completionBlock) } else if actorType == "users" { return getUserAvatar(forId: actorId, withStyle: style, usingAccount: account, completionBlock: completionBlock) } else if actorType == "federated_users" { return getFederatedUserAvatar(forId: actorId, withRoomToken: roomToken, withStyle: style, usingAccount: account, completionBlock: completionBlock) - } else if actorType == "deleted_users" { - return getDeletedUserAvatar(completionBlock: completionBlock) } } var image: UIImage? - if actorType == NCAttendeeTypeEmail { - image = self.getMailAvatar(with: style) + if actorType == NCAttendeeTypeEmail || actorType == NCAttendeeTypeGuest { + image = self.getGuestsAvatar(withDisplayName: actorDisplayName ?? "", withStyle: style) } else if actorType == NCAttendeeTypeGroup || actorType == NCAttendeeTypeCircle { image = self.getGroupAvatar(with: style) + } else if actorType == "deleted_users" { + image = self.getDeletedUserAvatar() } else { image = NCUtils.getImage(withString: "?", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true) } @@ -112,26 +110,20 @@ import SDWebImage return nil } - private func getGuestsAvatar(withDisplayName actorDisplayName: String, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { - let name = actorDisplayName.isEmpty ? "?" : actorDisplayName - let image = NCUtils.getImage(withString: name, withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true) - - completionBlock(image) + private func getGuestsAvatar(withDisplayName actorDisplayName: String, withStyle style: UIUserInterfaceStyle) -> UIImage? { + if actorDisplayName.isEmpty { + let traitCollection = UITraitCollection(userInterfaceStyle: style) + return UIImage(named: "user-avatar", in: nil, compatibleWith: traitCollection) + } - return nil + return NCUtils.getImage(withString: actorDisplayName, withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true) } - private func getDeletedUserAvatar(completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { - let image = NCUtils.getImage(withString: "X", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true) - - completionBlock(image) - - return nil + private func getDeletedUserAvatar() -> UIImage? { + return NCUtils.getImage(withString: "X", withBackgroundColor: .systemGray3, withBounds: self.avatarDefaultSize, isCircular: true) } - private func getUserAvatar(forId actorId: String, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { - let account = account ?? NCDatabaseManager.sharedInstance().activeAccount() - + private func getUserAvatar(forId actorId: String, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { return NCAPIController.sharedInstance().getUserAvatar(forUser: actorId, using: account, with: style) { image, _ in if image != nil { completionBlock(image) @@ -144,9 +136,7 @@ import SDWebImage } } - private func getFederatedUserAvatar(forId actorId: String, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount?, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { - let account = account ?? NCDatabaseManager.sharedInstance().activeAccount() - + private func getFederatedUserAvatar(forId actorId: String, withRoomToken roomToken: String?, withStyle style: UIUserInterfaceStyle, usingAccount account: TalkAccount, completionBlock: @escaping (_ image: UIImage?) -> Void) -> SDWebImageCombinedOperation? { return NCAPIController.sharedInstance().getFederatedUserAvatar(forUser: actorId, inRoom: roomToken, using: account, with: style) { image, _ in if image != nil { completionBlock(image) diff --git a/NextcloudTalk/AvatarProtocol.swift b/NextcloudTalk/AvatarProtocol.swift new file mode 100644 index 000000000..bfd72cdad --- /dev/null +++ b/NextcloudTalk/AvatarProtocol.swift @@ -0,0 +1,18 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +protocol AvatarProtocol { + + func cancelCurrentRequest() + + // MARK: - Conversation avatars + func setAvatar(for room: NCRoom) + func setGroupAvatar() + + // MARK: - User avatars + func setActorAvatar(forMessage message: NCChatMessage, withAccount account: TalkAccount) + func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount) + +} diff --git a/NextcloudTalk/AvatarView.swift b/NextcloudTalk/AvatarView.swift new file mode 100644 index 000000000..dd18aa881 --- /dev/null +++ b/NextcloudTalk/AvatarView.swift @@ -0,0 +1,210 @@ +// +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import UIKit +import SDWebImage + +@objcMembers class AvatarView: UIView, AvatarProtocol { + + private let userStatusSizePercentage = 0.38 + + public let avatarImageView = AvatarImageView(frame: .zero) + public let favoriteImageView = UIImageView() + private let userStatusImageView = UIImageView() + private let userStatusLabel = UILabel() + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.commonInit() + } + + private func commonInit() { + self.addSubview(avatarImageView) + self.addSubview(favoriteImageView) + self.addSubview(userStatusImageView) + self.addSubview(userStatusLabel) + + avatarImageView.contentMode = .scaleAspectFit + favoriteImageView.contentMode = .scaleAspectFill + userStatusImageView.contentMode = .center + userStatusLabel.textAlignment = .center + + userStatusImageView.isHidden = true + userStatusLabel.isHidden = true + + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + favoriteImageView.translatesAutoresizingMaskIntoConstraints = false + userStatusImageView.translatesAutoresizingMaskIntoConstraints = false + userStatusLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 0), + avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0), + avatarImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0), + + favoriteImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: userStatusSizePercentage), + favoriteImageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: userStatusSizePercentage), + favoriteImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: 2), + favoriteImageView.topAnchor.constraint(equalTo: topAnchor, constant: -4), + + userStatusImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: userStatusSizePercentage), + userStatusImageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: userStatusSizePercentage), + userStatusImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: 2), + userStatusImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 2), + + userStatusLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: userStatusSizePercentage), + userStatusLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: userStatusSizePercentage), + userStatusLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 2), + userStatusLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 2) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + + avatarImageView.layer.cornerRadius = avatarImageView.frame.height / 2 + avatarImageView.clipsToBounds = true + + userStatusImageView.layer.cornerRadius = userStatusImageView.frame.height / 2 + userStatusImageView.clipsToBounds = true + } + + public func prepareForReuse() { + // Fix problem of rendering downloaded image in a reused cell + avatarImageView.cancelCurrentRequest() + avatarImageView.image = nil + + favoriteImageView.image = nil + + userStatusImageView.image = nil + userStatusImageView.backgroundColor = .clear + + userStatusLabel.text = nil + } + + func cancelCurrentRequest() { + self.avatarImageView.cancelCurrentRequest() + } + + // MARK: - Conversation avatars + + public func setAvatar(for room: NCRoom) { + self.avatarImageView.setAvatar(for: room) + } + + public func setGroupAvatar() { + self.avatarImageView.setGroupAvatar() + } + + public func setMailAvatar() { + self.avatarImageView.setMailAvatar() + } + + // MARK: - User avatars + + public func setActorAvatar(forMessage message: NCChatMessage, withAccount account: TalkAccount) { + self.avatarImageView.setActorAvatar(forMessage: message, withAccount: account) + } + + public func setActorAvatar(forId actorId: String?, withType actorType: String?, withDisplayName actorDisplayName: String?, withRoomToken roomToken: String?, using account: TalkAccount) { + self.avatarImageView.setActorAvatar(forId: actorId, withType: actorType, withDisplayName: actorDisplayName, withRoomToken: roomToken, using: account) + } + + // MARK: - User status + + public func setStatus(for room: NCRoom, with backgroundColor: UIColor) { + if room.type == .oneToOne, let roomStatus = room.status { + if roomStatus != "dnd", let roomStatusIcon = room.statusIcon { + setUserStatusIcon(roomStatusIcon, with: backgroundColor) + } else { + setUserStatus(roomStatus, with: backgroundColor) + } + } else if room.isPublic { + if let statusImage = UIImage(named: "link") { + let diameter = statusImageSize(padding: 2) + let size = CGSize(width: diameter, height: diameter) + if let configuredImage = NCUtils.renderAspectImage(image: statusImage, ofSize: size, centerImage: true)?.withRenderingMode(.alwaysTemplate) { + setUserStatusImage(configuredImage, with: backgroundColor) + userStatusImageView.tintColor = .label + } + } + } else if room.isFederated { + if let statusImage = statusImageWith(name: "globe", color: .label, padding: 3) { + setUserStatusImage(statusImage, with: backgroundColor) + } + } + } + + private func setUserStatus(_ userStatus: String, with backgroundColor: UIColor) { + if userStatus == "online" { + if let statusImage = statusImageWith(name: "circle.fill", color: .systemGreen, padding: 2) { + setUserStatusImage(statusImage, with: backgroundColor) + } + } else if userStatus == "away" { + if let statusImage = statusImageWith(name: "moon.fill", color: .systemYellow, padding: 2) { + setUserStatusImage(statusImage, with: backgroundColor) + } + } else if userStatus == "dnd" { + var dndImageName = "minus.circle.fill" + if #available(iOS 16.1, *) { + dndImageName = "wrongwaysign.fill" + } + if let statusImage = statusImageWith(name: dndImageName, color: .systemRed, secondaryColor: .white, padding: 2) { + setUserStatusImage(statusImage, with: backgroundColor) + } + } + } + + private func setUserStatusImage(_ userStatusImage: UIImage, with backgroundColor: UIColor) { + userStatusImageView.backgroundColor = backgroundColor + userStatusImageView.image = userStatusImage + + userStatusImageView.isHidden = false + userStatusLabel.isHidden = true + } + + private func setUserStatusIcon(_ userStatusIcon: String, with backgroundColor: UIColor) { + userStatusLabel.text = userStatusIcon + userStatusLabel.font = .systemFont(ofSize: userStatusLabel.frame.height - 6) + + userStatusImageView.isHidden = true + userStatusLabel.isHidden = false + } + + private func statusImageWith(name: String, color: UIColor, secondaryColor: UIColor? = nil, padding: CGFloat) -> UIImage? { + let sizeConfiguration = UIImage.SymbolConfiguration(pointSize: statusImageSize(padding: padding)) + + // Multicolor image + if let secondaryColor { + let colorConfiguration = UIImage.SymbolConfiguration(paletteColors: [secondaryColor, color]) + let combinedSymbolConfiguration = colorConfiguration.applying(sizeConfiguration) + if let statusImage = UIImage(systemName: name)? + .applyingSymbolConfiguration(combinedSymbolConfiguration) { + return statusImage + } + } + + // Single color image + if let statusImage = UIImage(systemName: name)? + .withTintColor(color, renderingMode: .alwaysOriginal) + .applyingSymbolConfiguration(sizeConfiguration) { + return statusImage + } + + return nil + } + + private func statusImageSize(padding: CGFloat) -> CGFloat { + return self.frame.size.height * userStatusSizePercentage - padding * 2 + } +} diff --git a/NextcloudTalk/BadgeView.swift b/NextcloudTalk/BadgeView.swift new file mode 100644 index 000000000..7cccecb95 --- /dev/null +++ b/NextcloudTalk/BadgeView.swift @@ -0,0 +1,106 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +class BadgeView: UIView { + + enum BadgeHighlightStyle { + case none + case border + case important + } + + private let badgeNumberLimit = 9999 + + private let defaultBadgeColor: UIColor = .systemGray3 + private let defaultBadgeTextColor: UIColor = .label + + private let horizontalPadding: CGFloat = 8 + private let verticalPadding: CGFloat = 4 + + private let label = UILabel() + + var badgeColor: UIColor = .red + var badgeTextColor: UIColor = .white + var badgeHighlightStyle: BadgeHighlightStyle = .important + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + label.textAlignment = .center + label.textColor = .label + label.font = UIFont.preferredFont(for: .footnote, weight: .bold) + label.adjustsFontForContentSizeCategory = true + label.layer.masksToBounds = true + + self.addSubview(label) + } + + func setBadgeNumber(_ number: Int) { + if number == 0 { + label.text = "" + invalidateIntrinsicContentSize() + return + } + + if number > badgeNumberLimit { + label.text = "\(badgeNumberLimit)+" + } else { + label.text = "\(number)" + } + + let width = label.intrinsicContentSize.width + horizontalPadding * 2 + let height = label.intrinsicContentSize.height + verticalPadding * 2 + let finalWidth = width < height ? height : width + + // Perform badge view resizing and setting badge color without animations + UIView.performWithoutAnimation { + layer.cornerRadius = height / 2 + frame.size = CGSize(width: finalWidth, height: height) + + label.frame.size = CGSize(width: finalWidth, height: height) + label.center = CGPoint(x: finalWidth / 2, y: height / 2) + + setBadgeColor(style: badgeHighlightStyle) + } + + invalidateIntrinsicContentSize() + } + + func setBadgeColor(style: BadgeHighlightStyle) { + switch style { + case .none: + layer.borderWidth = 0 + backgroundColor = defaultBadgeColor + label.textColor = defaultBadgeTextColor + case .border: + layer.borderWidth = 2 + layer.borderColor = badgeColor.cgColor + backgroundColor = .clear + label.textColor = badgeColor + case .important: + layer.borderWidth = 0 + backgroundColor = badgeColor + label.textColor = badgeTextColor + } + } + + override var intrinsicContentSize: CGSize { + guard let text = label.text, !text.isEmpty else { return .zero } + + let width = label.intrinsicContentSize.width + horizontalPadding * 2 + let height = label.intrinsicContentSize.height + verticalPadding * 2 + let finalWidth = width < height ? height : width + + return CGSize(width: finalWidth, height: height) + } +} diff --git a/NextcloudTalk/BannedActorTableViewController.swift b/NextcloudTalk/BannedActorTableViewController.swift index 481583647..892a172f6 100644 --- a/NextcloudTalk/BannedActorTableViewController.swift +++ b/NextcloudTalk/BannedActorTableViewController.swift @@ -54,9 +54,7 @@ } func getData() { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - - NCAPIController.sharedInstance().listBans(for: activeAccount.accountId, in: room.token) { [weak self] bannedActors in + NCAPIController.sharedInstance().listBans(for: room.accountId, in: room.token) { [weak self] bannedActors in guard let self else { return } self.bannedActors = bannedActors ?? [] @@ -105,9 +103,7 @@ self.showActivityIndicator() cell.setDisabledState() - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - - NCAPIController.sharedInstance().unbanActor(for: activeAccount.accountId, in: self.room.token, with: bannedActor.banId) { [weak self] success in + NCAPIController.sharedInstance().unbanActor(for: room.accountId, in: self.room.token, with: bannedActor.banId) { [weak self] success in if !success { NotificationPresenter.shared().present(text: NSLocalizedString("Failed to unban selected entry", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) } diff --git a/NextcloudTalk/BaseChatTableViewCell+File.swift b/NextcloudTalk/BaseChatTableViewCell+File.swift index 16052b034..924280aa8 100644 --- a/NextcloudTalk/BaseChatTableViewCell+File.swift +++ b/NextcloudTalk/BaseChatTableViewCell+File.swift @@ -123,14 +123,30 @@ extension BaseChatTableViewCell { return } + var placeholderImage: UIImage? + var previewImageHeight: CGFloat? + var previewImageWidth: CGFloat? + // In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly - if file.previewImageHeight > 0 { - self.filePreviewImageViewHeightConstraint?.constant = CGFloat(file.previewImageHeight) + if file.previewImageHeight > 0 && file.previewImageWidth > 0 { + previewImageHeight = CGFloat(file.previewImageHeight) + previewImageWidth = CGFloat(file.previewImageWidth) } else { - let estimatedPreviewHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message) + let estimatedPreviewSize = BaseChatTableViewCell.getEstimatedPreviewSize(for: message) + + if estimatedPreviewSize.height > 0 && estimatedPreviewSize.width > 0 { + previewImageHeight = estimatedPreviewSize.height + previewImageWidth = estimatedPreviewSize.width + } + } + + if let previewImageHeight, let previewImageWidth { + self.filePreviewImageViewHeightConstraint?.constant = previewImageHeight + self.filePreviewImageViewWidthConstraint?.constant = previewImageWidth - if estimatedPreviewHeight > 0 { - self.filePreviewImageViewHeightConstraint?.constant = estimatedPreviewHeight + if !message.isAnimatableGif, let blurhash = message.file()?.blurhash { + let aspectRatio = previewImageHeight / previewImageWidth + placeholderImage = .init(blurHash: blurhash, size: .init(width: 20, height: 20 * aspectRatio)) } } @@ -140,7 +156,7 @@ extension BaseChatTableViewCell { if message.isAnimatableGif { self.requestGifPreview(for: message, with: account) } else { - self.requestDefaultPreview(for: message, with: account) + self.requestDefaultPreview(for: message, withPlaceholderImage: placeholderImage, with: account) } } @@ -162,7 +178,7 @@ extension BaseChatTableViewCell { let gifImage = try? UIImage(gifData: data), let baseImage = UIImage(data: data) else { // No gif, try to request a normal preview - self.requestDefaultPreview(for: message, with: account) + self.requestDefaultPreview(for: message, withPlaceholderImage: nil, with: account) return } @@ -171,13 +187,13 @@ extension BaseChatTableViewCell { } } - func requestDefaultPreview(for message: NCChatMessage, with account: TalkAccount) { + func requestDefaultPreview(for message: NCChatMessage, withPlaceholderImage placeholderImage: UIImage?, with account: TalkAccount) { guard let file = message.file() else { return } let requestedHeight = Int(3 * fileMessageCellFileMaxPreviewHeight) guard let previewRequest = NCAPIController.sharedInstance().createPreviewRequest(forFile: file.parameterId, withMaxHeight: requestedHeight, using: account) else { return } - self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: nil, success: { [weak self] _, _, image in + self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: placeholderImage, success: { [weak self] _, _, image in guard let self, let imageView = self.filePreviewImageView else { return } imageView.image = image @@ -218,7 +234,7 @@ extension BaseChatTableViewCell { self.filePreviewPlayIconImageView?.center = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0) } - self.delegate?.cellHasDownloadedImagePreview(withHeight: ceil(previewSize.height), for: message) + self.delegate?.cellHasDownloadedImagePreview(withSize: .init(width: ceil(previewSize.width), height: ceil(previewSize.height)), for: message) } func showFallbackIcon(for message: NCChatMessage) { @@ -284,22 +300,22 @@ extension BaseChatTableViewCell { return CGSize(width: width, height: height) } - static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGFloat { - guard let message, let fileParameter = message.file() else { return 0 } + static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGSize { + guard let message, let fileParameter = message.file() else { return .zero } // We don't have any information about the image to display if fileParameter.width == 0 && fileParameter.height == 0 { - return 0 + return .zero } // We can only estimate the height for images and videos if !NCUtils.isVideo(fileType: fileParameter.mimetype), !NCUtils.isImage(fileType: fileParameter.mimetype) { - return 0 + return .zero } let imageSize = CGSize(width: CGFloat(fileParameter.width), height: CGFloat(fileParameter.height)) let previewSize = self.getPreviewSize(from: imageSize, true) - return ceil(previewSize.height) + return .init(width: ceil(previewSize.width), height: ceil(previewSize.height)) } } diff --git a/NextcloudTalk/BaseChatTableViewCell+Message.swift b/NextcloudTalk/BaseChatTableViewCell+Message.swift index 5faa46f11..b625c9af4 100644 --- a/NextcloudTalk/BaseChatTableViewCell+Message.swift +++ b/NextcloudTalk/BaseChatTableViewCell+Message.swift @@ -26,8 +26,4 @@ extension BaseChatTableViewCell { messageTextView.attributedText = message.parsedMarkdownForChat() } - - func prepareForReuseMessageCell() { - self.messageTextView?.text = "" - } } diff --git a/NextcloudTalk/BaseChatTableViewCell+Poll.swift b/NextcloudTalk/BaseChatTableViewCell+Poll.swift index 33ba899fd..68c7b6159 100644 --- a/NextcloudTalk/BaseChatTableViewCell+Poll.swift +++ b/NextcloudTalk/BaseChatTableViewCell+Poll.swift @@ -37,10 +37,6 @@ extension BaseChatTableViewCell { pollMessageView.pollTitleTextView.text = message.parsedMessage().string } - func prepareForReusePollCell() { - self.pollMessageView?.pollTitleTextView.text = "" - } - @objc func pollViewTapped() { guard let poll = message?.poll else { return diff --git a/NextcloudTalk/BaseChatTableViewCell.swift b/NextcloudTalk/BaseChatTableViewCell.swift index 3647b9bb2..3961a7df7 100644 --- a/NextcloudTalk/BaseChatTableViewCell.swift +++ b/NextcloudTalk/BaseChatTableViewCell.swift @@ -13,7 +13,7 @@ protocol BaseChatTableViewCellDelegate: AnyObject { func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage) func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage) - func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) + func cellHasDownloadedImagePreview(withSize size: CGSize, for message: NCChatMessage) func cellWants(toOpenLocation geoLocationRichObject: GeoLocationRichObject) @@ -77,6 +77,7 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions public var message: NCChatMessage? public var room: NCRoom? + public var account: TalkAccount? internal var quotedMessageView: QuotedMessageView? internal var reactionView: ReactionsView? @@ -131,24 +132,16 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions self.quotedMessageView?.avatarView.cancelCurrentRequest() self.quotedMessageView?.avatarView.image = nil - self.titleLabel.text = "" - self.dateLabel.text = "" - self.headerPart.isHidden = false self.quotePart.isHidden = true self.referencePart.isHidden = true self.reactionPart.isHidden = true - self.statusView.isHidden = false - self.statusView.subviews.forEach { $0.removeFromSuperview() } - self.referenceView?.prepareForReuse() - self.prepareForReuseMessageCell() self.prepareForReuseFileCell() self.prepareForReuseLocationCell() self.prepareForReuseAudioCell() - self.prepareForReusePollCell() if let replyGestureRecognizer { self.removeGestureRecognizer(replyGestureRecognizer) @@ -157,11 +150,12 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions } // swiftlint:disable:next cyclomatic_complexity - public func setup(for message: NCChatMessage, inRoom room: NCRoom) { + public func setup(for message: NCChatMessage, inRoom room: NCRoom, withAccount account: TalkAccount) { self.message = message self.room = room + self.account = account - self.avatarButton.setActorAvatar(forMessage: message) + self.avatarButton.setActorAvatar(forMessage: message, withAccount: account) self.avatarButton.menu = self.getDeferredUserMenu() self.avatarButton.showsMenuAsPrimaryAction = true @@ -189,7 +183,6 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions self.titleLabel.attributedText = titleLabel - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() let shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReadStatus, for: room) var shouldShowReadStatus = false @@ -204,15 +197,15 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions let quoteString = parent.parsedMarkdownForChat()?.string ?? "" self.quotedMessageView?.messageLabel.text = quoteString self.quotedMessageView?.actorLabel.attributedText = parent.actor.attributedDisplayName - self.quotedMessageView?.highlighted = parent.isMessage(from: activeAccount.userId) - self.quotedMessageView?.avatarView.setActorAvatar(forMessage: parent) + self.quotedMessageView?.highlighted = parent.isMessage(from: account.userId) + self.quotedMessageView?.avatarView.setActorAvatar(forMessage: parent, withAccount: account) } if message.isGroupMessage, message.parent == nil { self.headerPart.isHidden = true } - // When `setDeliveryState` is not called, we still need to make sure the placeholder view is removed + // Make sure the status view is empty, when no delivery state should be set self.statusView.subviews.forEach { $0.removeFromSuperview() } if message.isDeleting { @@ -221,7 +214,7 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions self.setDeliveryState(to: .failed) } else if message.isTemporary { self.setDeliveryState(to: .sending) - } else if message.isMessage(from: activeAccount.userId), shouldShowDeliveryStatus { + } else if message.isMessage(from: account.userId), shouldShowDeliveryStatus { if room.lastCommonReadMessage >= message.messageId, shouldShowReadStatus { self.setDeliveryState(to: .read) } else { @@ -269,7 +262,7 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions self.setupForPollCell(with: message) } else if message.file() != nil { // File message - self.setupForFileCell(with: message, with: activeAccount) + self.setupForFileCell(with: message, with: account) } else if message.geoLocation() != nil { // Location message self.setupForLocationCell(with: message) @@ -281,6 +274,8 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions if message.isDeletedMessage { self.statusView.isHidden = true self.messageTextView?.textColor = .tertiaryLabel + } else { + self.statusView.isHidden = false } NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil) @@ -318,8 +313,6 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions } func setDeliveryState(to deliveryState: ChatMessageDeliveryState) { - self.statusView.subviews.forEach { $0.removeFromSuperview() } - if deliveryState == .sending || deliveryState == .deleting { let activityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20)) @@ -461,11 +454,10 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions // MARK: - Avatar User Menu func getDeferredUserMenu() -> UIMenu? { - guard let message = self.message else { return nil } - - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + guard let message = self.message, let account = message.account + else { return nil } - if message.actorType != "users" || message.actorId == activeAccount.userId { + if message.actorType != "users" || message.actorId == account.userId { return nil } @@ -480,9 +472,9 @@ class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, Reactions } func getMenuUserAction(for message: NCChatMessage, completionBlock: @escaping ([UIMenuElement]) -> Void) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + guard let account = message.account else { return } - NCAPIController.sharedInstance().getUserActions(forUser: message.actorId, using: activeAccount) { userActionsRaw, error in + NCAPIController.sharedInstance().getUserActions(forUser: message.actorId, using: account) { userActionsRaw, error in guard error == nil, let userActionsDict = userActionsRaw as? [String: AnyObject], let userActions = userActionsDict["actions"] as? [[String: String]], diff --git a/NextcloudTalk/BaseChatTableViewCell.xib b/NextcloudTalk/BaseChatTableViewCell.xib index 41f41f612..0ab6555cc 100644 --- a/NextcloudTalk/BaseChatTableViewCell.xib +++ b/NextcloudTalk/BaseChatTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -70,16 +70,8 @@ - + - - - - - - - - diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index ca54a8e5a..1749fb351 100644 --- a/NextcloudTalk/BaseChatViewController.swift +++ b/NextcloudTalk/BaseChatViewController.swift @@ -10,6 +10,7 @@ import UIKit import Realm import ContactsUI import QuickLook +import SwiftUI @objcMembers public class BaseChatViewController: InputbarViewController, UITextFieldDelegate, @@ -30,7 +31,8 @@ import QuickLook AVAudioPlayerDelegate, SystemMessageTableViewCellDelegate, BaseChatTableViewCellDelegate, - UITableViewDataSourcePrefetching { + UITableViewDataSourcePrefetching, + MessageSeparatorTableViewCellDelegate { // MARK: - Internal var internal var messages: [Date: [NCChatMessage]] = [:] @@ -80,6 +82,8 @@ import QuickLook private var sendButtonTagMessage = 99 private var sendButtonTagVoice = 98 + private var isVoiceRecordingLocked = false + private var actionTypeTranscribeVoiceMessage = "transcribe-voice-message" private var imagePicker: UIImagePickerController? @@ -89,6 +93,7 @@ import QuickLook private var voiceMessageLongPressGesture: UILongPressGestureRecognizer? private var recorder: AVAudioRecorder? private var voiceMessageRecordingView: VoiceMessageRecordingView? + private var expandedUIHostingController: UIHostingController? private var longPressStartingPoint: CGPoint? private var cancelHintLabelInitialPositionX: CGFloat? private var recordCancelled: Bool = false @@ -169,10 +174,26 @@ import QuickLook return button }() + private lazy var voiceRecordingLockButton: UIButton = { + let button = UIButton(frame: .init(x: 0, y: 0, width: 44, height: 44)) + + button.backgroundColor = .secondarySystemBackground + button.tintColor = .systemBlue + button.layer.cornerRadius = button.frame.size.height / 2 + button.clipsToBounds = true + button.alpha = 0 + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage(systemName: "lock.open"), for: .normal) + + self.view.addSubview(button) + + return button + }() + // MARK: - Init/Deinit - public init?(for room: NCRoom) { - super.init(for: room, tableViewStyle: .plain) + public init?(forRoom room: NCRoom, withAccount account: TalkAccount) { + super.init(forRoom: room, withAccount: account, tableViewStyle: .plain) self.hidesBottomBarWhenPushed = true self.tableView?.estimatedRowHeight = 0 @@ -188,8 +209,8 @@ import QuickLook // Not using an optional here, because it is not available from ObjC // Pass "0" as highlightMessageId to not highlight a message - public convenience init?(for room: NCRoom, withMessage messages: [NCChatMessage], withHighlightId highlightMessageId: Int) { - self.init(for: room) + public convenience init?(forRoom room: NCRoom, withAccount account: TalkAccount, withMessage messages: [NCChatMessage], withHighlightId highlightMessageId: Int) { + self.init(forRoom: room, withAccount: account) // When we pass in a fixed number of messages, we hide the inputbar by default self.textInputbar.isHidden = true @@ -250,8 +271,7 @@ import QuickLook self.tableView?.register(UINib(nibName: "BaseChatTableViewCell", bundle: nil), forCellReuseIdentifier: pollGroupedMessageCellIdentifier) self.tableView?.register(SystemMessageTableViewCell.self, forCellReuseIdentifier: SystemMessageCellIdentifier) - self.tableView?.register(SystemMessageTableViewCell.self, forCellReuseIdentifier: InvisibleSystemMessageCellIdentifier) - self.tableView?.register(MessageSeparatorTableViewCell.self, forCellReuseIdentifier: MessageSeparatorCellIdentifier) + self.tableView?.register(MessageSeparatorTableViewCell.self, forCellReuseIdentifier: MessageSeparatorTableViewCell.identifier) let newMessagesButtonText = NSLocalizedString("↓ New messages", comment: "") @@ -264,7 +284,8 @@ import QuickLook "unreadMessageButton": self.unreadMessageButton, "textInputbar": self.textInputbar, "scrollToBottomButton": self.scrollToBottomButton, - "autoCompletionView": self.autoCompletionView + "autoCompletionView": self.autoCompletionView, + "voiceRecordingLockButton": self.voiceRecordingLockButton ] let metrics = [ @@ -281,11 +302,17 @@ import QuickLook self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[scrollToBottomButton(44)]-10-[autoCompletionView]", metrics: metrics, views: views)) self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[scrollToBottomButton(44)]-(>=0)-|", metrics: metrics, views: views)) + self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[voiceRecordingLockButton(44)]-64-[autoCompletionView]", metrics: metrics, views: views)) + self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=0)-[voiceRecordingLockButton(44)]-(>=0)-|", metrics: metrics, views: views)) + self.scrollToBottomButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true + self.voiceRecordingLockButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true self.addMenuToLeftButton() self.replyMessageView?.addObserver(self, forKeyPath: "visible", options: .new, context: nil) + + self.textView.pastableMediaTypes = .images } // swiftlint:disable:next block_based_kvo @@ -395,6 +422,14 @@ import QuickLook if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { self.updateToolbar(animated: true) } + + if self.traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass, + let indexPath = self.indexPathForUnreadMessageSeparator() { + + DispatchQueue.main.async { + self.tableView?.reloadRows(at: [indexPath], with: .none) + } + } } // MARK: - Keyboard notifications @@ -467,11 +502,10 @@ import QuickLook internal func createTemporaryMessage(message: String, replyTo parentMessage: NCChatMessage?, messageParameters: String, silently: Bool, isVoiceMessage: Bool) -> NCChatMessage { let temporaryMessage = NCChatMessage() - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - temporaryMessage.accountId = activeAccount.accountId - temporaryMessage.actorDisplayName = activeAccount.userDisplayName - temporaryMessage.actorId = activeAccount.userId + temporaryMessage.accountId = self.account.accountId + temporaryMessage.actorDisplayName = self.account.userDisplayName + temporaryMessage.actorId = self.account.userId temporaryMessage.actorType = "users" temporaryMessage.timestamp = Int(Date().timeIntervalSince1970) temporaryMessage.token = room.token @@ -871,12 +905,11 @@ import QuickLook func showReplyView(for message: NCChatMessage) { let isAtBottom = self.shouldScrollOnNewMessages() - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() if let replyProxyView = self.replyProxyView as? ReplyMessageView { self.replyMessageView = replyProxyView - replyProxyView.presentReply(with: message, withUserId: activeAccount.userId) + replyProxyView.presentReply(with: message, withUserId: self.account.userId) self.presentKeyboard(true) // Make sure we're really at the bottom after showing the replyMessageView @@ -948,13 +981,11 @@ import QuickLook } func didPressNoteToSelf(for message: NCChatMessage) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - - NCAPIController.sharedInstance().getNoteToSelfRoom(forAccount: activeAccount) { roomDict, error in - if error == nil, let room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) { + NCAPIController.sharedInstance().getNoteToSelfRoom(forAccount: self.account) { roomDict, error in + if error == nil, let room = NCRoom(dictionary: roomDict, andAccountId: self.account.accountId) { if message.isObjectShare { - NCAPIController.sharedInstance().shareRichObject(message.richObjectFromObjectShare, inRoom: room.token, for: activeAccount) { error in + NCAPIController.sharedInstance().shareRichObject(message.richObjectFromObjectShare, inRoom: room.token, for: self.account) { error in if error == nil { NotificationPresenter.shared().present(text: NSLocalizedString("Added note to self", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success) } else { @@ -962,7 +993,7 @@ import QuickLook } } } else { - NCAPIController.sharedInstance().sendChatMessage(message.parsedMessage().string, toRoom: room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: activeAccount) { error in + NCAPIController.sharedInstance().sendChatMessage(message.parsedMessage().string, toRoom: room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: self.account) { error in if error == nil { NotificationPresenter.shared().present(text: NSLocalizedString("Added note to self", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success) } else { @@ -1021,8 +1052,7 @@ import QuickLook } func didPressTranslate(for message: NCChatMessage) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - let translateMessageVC = MessageTranslationViewController(message: message.parsedMessage().string, availableTranslations: NCDatabaseManager.sharedInstance().availableTranslations(forAccountId: activeAccount.accountId)) + let translateMessageVC = MessageTranslationViewController(message: message.parsedMessage().string, availableTranslations: NCDatabaseManager.sharedInstance().availableTranslations(forAccountId: self.room.accountId)) self.presentWithNavigation(translateMessageVC, animated: true) } @@ -1116,9 +1146,7 @@ import QuickLook self.updateMessage(withMessageId: deletingMessage.messageId, updatedMessage: deletingMessage) } - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - - NCAPIController.sharedInstance().deleteChatMessage(inRoom: self.room.token, withMessageId: message.messageId, for: activeAccount) { messageDict, error, statusCode in + NCAPIController.sharedInstance().deleteChatMessage(inRoom: self.room.token, withMessageId: message.messageId, for: self.account) { messageDict, error, statusCode in if error == nil, let messageDict, let parent = messageDict["parent"] as? [AnyHashable: Any] { @@ -1129,7 +1157,7 @@ import QuickLook NotificationPresenter.shared().present(text: NSLocalizedString("Message deleted successfully", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success) } - if let deleteMessage = NCChatMessage(dictionary: parent, andAccountId: activeAccount.accountId) { + if let deleteMessage = NCChatMessage(dictionary: parent, andAccountId: self.account.accountId) { self.updateMessage(withMessageId: deleteMessage.messageId, updatedMessage: deleteMessage) } } else if error != nil { @@ -1332,7 +1360,7 @@ import QuickLook // MARK: - ShareConfirmationViewController delegate & helper - public func shareConfirmationViewControllerDidFailed(_ viewController: ShareConfirmationViewController) { + public func shareConfirmationViewControllerDidFail(_ viewController: ShareConfirmationViewController) { self.dismiss(animated: true) { if viewController.forwardingMessage { NotificationPresenter.shared().present(text: NSLocalizedString("Failed to forward message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) @@ -1351,10 +1379,14 @@ import QuickLook } } - internal func createShareConfirmationViewController() -> (shareConfirmationVC: ShareConfirmationViewController, navController: NCNavigationController) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) - let shareConfirmationVC = ShareConfirmationViewController(room: self.room, account: activeAccount, serverCapabilities: serverCapabilities!)! + public func shareConfirmationViewControllerDidCancel(_ viewController: ShareConfirmationViewController) { + self.setChatMessage(viewController.textView.text) + self.dismiss(animated: true) + } + + internal func createShareConfirmationViewController() -> (shareConfirmationVC: ShareConfirmationViewController, navController: NCNavigationController)? { + let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.account.accountId) + let shareConfirmationVC = ShareConfirmationViewController(room: self.room, account: self.account, serverCapabilities: serverCapabilities!)! shareConfirmationVC.delegate = self shareConfirmationVC.isModal = true let navigationController = NCNavigationController(rootViewController: shareConfirmationVC) @@ -1368,6 +1400,22 @@ import QuickLook self.dismiss(animated: true) } + // MARK: - TextView paste support + + public override func didPasteMediaContent(_ userInfo: [AnyHashable: Any]) { + guard let data = userInfo[SLKTextViewPastedItemData] as? Data, + let image = UIImage(data: data), + let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() + else { return } + + shareConfirmationVC.setChatMessage(self.textView.text) + self.setChatMessage("") + + self.present(navigationController, animated: true) { + shareConfirmationVC.shareItemController.addItem(with: image) + } + } + // MARK: - PHPhotoPicker Delegate public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { @@ -1376,7 +1424,7 @@ import QuickLook return } - let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() + guard let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() else { return } picker.dismiss(animated: true) { self.present(navigationController, animated: true) { @@ -1419,9 +1467,9 @@ import QuickLook public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { self.saveImagePickerSettings(picker) - let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() - - guard let mediaType = info[.mediaType] as? String else { return } + guard let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController(), + let mediaType = info[.mediaType] as? String + else { return } if mediaType == "public.image" { guard let image = info[.originalImage] as? UIImage else { return } @@ -1456,7 +1504,7 @@ import QuickLook // MARK: - UIDocumentPickerViewController Delegate public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() + guard let (shareConfirmationVC, navigationController) = self.createShareConfirmationViewController() else { return } self.present(navigationController, animated: true) { for url in urls { @@ -1469,9 +1517,8 @@ import QuickLook public func shareLocationViewController(_ viewController: ShareLocationViewController, didSelectLocationWithLatitude latitude: Double, longitude: Double, andName name: String) { let richObject = GeoLocationRichObject(latitude: latitude, longitude: longitude, name: name) - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().shareRichObject(richObject.richObjectDictionary(), inRoom: self.room.token, for: activeAccount) { error in + NCAPIController.sharedInstance().shareRichObject(richObject.richObjectDictionary(), inRoom: self.room.token, for: self.account) { error in if let error { print("Error sharing rich object: \(error)") } @@ -1483,7 +1530,8 @@ import QuickLook // MARK: - CNContactPickerViewController Delegate public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { - guard let vCardData = try? CNContactVCardSerialization.data(with: [contact]) else { return } + guard let vCardData = try? CNContactVCardSerialization.data(with: [contact]) + else { return } var vcString = String(data: vCardData, encoding: .utf8) @@ -1501,9 +1549,8 @@ import QuickLook try vcString?.write(toFile: filePath, atomically: true, encoding: .utf8) let url = URL(fileURLWithPath: filePath) let contactFileName = "\(contact.identifier).vcf" - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().uniqueNameForFileUpload(withName: contactFileName, originalName: true, for: activeAccount) { fileServerURL, fileServerPath, _, _ in + NCAPIController.sharedInstance().uniqueNameForFileUpload(withName: contactFileName, originalName: true, for: self.account) { fileServerURL, fileServerPath, _, _ in if let fileServerURL, let fileServerPath { self.uploadFileAtPath(localPath: url.path, withFileServerURL: fileServerURL, andFileServerPath: fileServerPath, withMetaData: nil, temporaryMessage: nil) } else { @@ -1548,6 +1595,72 @@ import QuickLook self.voiceMessageRecordingView?.isHidden = true } + // MARK: - Expanded voice message recording + + func showExpandedVoiceMessageRecordingView(offset: Int) { + let expandedView = ExpandedVoiceMessageRecordingView( + deleteFunc: handleDelete, sendFunc: handleSend, recordFunc: handleRecord(isRecording:), timeElapsed: offset + ) + + let hostingController = UIHostingController(rootView: expandedView) + guard let expandedVoiceMessageRecordingView = hostingController.view else { return } + + self.expandedUIHostingController = hostingController + self.view.addSubview(expandedVoiceMessageRecordingView) + + expandedVoiceMessageRecordingView.translatesAutoresizingMaskIntoConstraints = false + + let views = [ + "expandedVoiceMessageRecordingView": expandedVoiceMessageRecordingView + ] + + expandedVoiceMessageRecordingView.bottomAnchor.constraint(equalTo: self.textInputbar.bottomAnchor).isActive = true + self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[expandedVoiceMessageRecordingView]|", metrics: nil, views: views)) + } + + func handleDelete() { + self.recordCancelled = true + self.stopRecordingVoiceMessage() + handleCollapseVoiceRecording() + } + + func handleSend() { + if let recorder = self.recorder, recorder.isRecording { + self.recordCancelled = false + self.stopRecordingVoiceMessage() + } else { + self.hideVoiceMessageRecordingView() + self.shareVoiceMessage() + } + handleCollapseVoiceRecording() + } + + func handleRecord(isRecording: Bool) { + if isRecording { + if let recorder = self.recorder, !recorder.isRecording { + let session = AVAudioSession.sharedInstance() + try? session.setActive(true) + recorder.record() + print("Recording Restarted") + } + } else { + recordCancelled = true + if let recorder = self.recorder, recorder.isRecording { + recorder.stop() + let session = AVAudioSession.sharedInstance() + try? session.setActive(false) + print("Recording Stopped") + } + } + } + + func handleCollapseVoiceRecording() { + self.isVoiceRecordingLocked = false + self.expandedUIHostingController?.removeFromParent() + self.expandedUIHostingController?.view.isHidden = true + self.textInputbar.bringSubviewToFront(self.textInputbar) + } + func setupAudioRecorder() { guard let userDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last, let outputFileURL = NSURL.fileURL(withPathComponents: [userDocumentDirectory, "voice-message-recording.m4a"]) @@ -1598,6 +1711,7 @@ import QuickLook let session = AVAudioSession.sharedInstance() try? session.setActive(true) recorder.record() + print("Recording started") } } @@ -1607,6 +1721,7 @@ import QuickLook recorder.stop() let session = AVAudioSession.sharedInstance() try? session.setActive(false) + print("Recording Stopped") } } @@ -1876,14 +1991,19 @@ import QuickLook self.recordCancelled = false self.longPressStartingPoint = point self.cancelHintLabelInitialPositionX = voiceMessageRecordingView?.slideToCancelHintLabel?.frame.origin.x + self.voiceRecordingLockButton.alpha = 1 } else if gestureRecognizer.state == .ended { - print("Stop recording audio message") self.shouldLockInterfaceOrientation(lock: false) - if let recordingTime = self.recorder?.currentTime { - // Mark record as cancelled if audio message is no longer than one second - self.recordCancelled = recordingTime < 1 + self.resetVoiceRecordingLockButton() + + if !isVoiceRecordingLocked { + if let recordingTime = self.recorder?.currentTime { + // Mark record as cancelled if audio message is no longer than one second + self.recordCancelled = recordingTime < 1 + } + self.stopRecordingVoiceMessage() + print("Stop recording audio message") } - self.stopRecordingVoiceMessage() } else if gestureRecognizer.state == .changed { guard let longPressStartingPoint, let cancelHintLabelInitialPositionX, @@ -1892,6 +2012,7 @@ import QuickLook else { return } let slideX = longPressStartingPoint.x - point.x + let slideY = longPressStartingPoint.y - point.y // Only slide view to the left if slideX > 0 { @@ -1903,19 +2024,35 @@ import QuickLook slideToCancelHintLabel.alpha = (maxSlideX - slideX) / 100 // Cancel recording if slided more than maxSlideX - if slideX > maxSlideX, !self.recordCancelled { + if slideX > maxSlideX, !self.recordCancelled, !isVoiceRecordingLocked { print("Cancel recording audio message") // 'Cancelled' feedback (three sequential weak booms) AudioServicesPlaySystemSound(1521) self.recordCancelled = true self.stopRecordingVoiceMessage() + self.resetVoiceRecordingLockButton() + } + } + + if slideY > 0 { + let maxSlideY = 64.0 + if slideY > maxSlideY, !self.recordCancelled { + if !isVoiceRecordingLocked { + self.voiceRecordingLockButton.setImage(UIImage(systemName: "lock"), for: .normal) + let offset = self.voiceMessageRecordingView?.recordingTimeLabel?.getTimeCounted() + let intOffset = Int(offset!.magnitude) + showExpandedVoiceMessageRecordingView(offset: intOffset) + print("LOCKED") + isVoiceRecordingLocked = true + } } } } else if gestureRecognizer.state == .cancelled || gestureRecognizer.state == .failed { print("Gesture cancelled or failed -> Cancel recording audio message") self.shouldLockInterfaceOrientation(lock: false) self.recordCancelled = false + self.resetVoiceRecordingLockButton() self.stopRecordingVoiceMessage() } } @@ -1926,6 +2063,11 @@ import QuickLook } } + func resetVoiceRecordingLockButton() { + self.voiceRecordingLockButton.alpha = 0 + self.voiceRecordingLockButton.setImage(UIImage(systemName: "lock.open"), for: .normal) + } + // MARK: - UIScrollViewDelegate methods public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { @@ -2006,7 +2148,7 @@ import QuickLook if shouldAddBlockSeparator { // Chat block separator let blockSeparatorMessage = NCChatMessage() - blockSeparatorMessage.messageId = kChatBlockSeparatorIdentifier + blockSeparatorMessage.messageId = MessageSeparatorTableViewCell.chatBlockSeparatorId historyMessagesForSection?.append(blockSeparatorMessage) } @@ -2082,6 +2224,10 @@ import QuickLook private func internalAppendMessages(messages: [NCChatMessage], inDictionary dictionary: inout [Date: [NCChatMessage]]) { for newMessage in messages { + // Skip any update message, as that would still trigger some operations on the UITableView. + // Processing of update messages still happens when receiving new messages, so safe to skip here + guard !newMessage.isUpdateMessage else { continue } + let newMessageDate = Date(timeIntervalSince1970: TimeInterval(newMessage.timestamp)) let keyDate = self.getKeyForDate(date: newMessageDate, inDictionary: dictionary) @@ -2215,21 +2361,19 @@ import QuickLook collapseByMessage.collapsedMessages.add(newMessage.messageId as NSNumber) collapseByMessage.isCollapsed = true - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - var isUser0Self = false var isUser1Self = false if let userDict = collapseByMessage.messageParameters["user"] as? [String: Any] { - isUser0Self = userDict["id"] as? String == activeAccount.userId && userDict["type"] as? String == "user" + isUser0Self = userDict["id"] as? String == self.account.userId && userDict["type"] as? String == "user" } if let userDict = newMessage.messageParameters["user"] as? [String: Any] { - isUser1Self = userDict["id"] as? String == activeAccount.userId && userDict["type"] as? String == "user" + isUser1Self = userDict["id"] as? String == self.account.userId && userDict["type"] as? String == "user" } - let isActor0Self = collapseByMessage.actorId == activeAccount.userId && collapseByMessage.actorType == "users" - let isActor1Self = newMessage.actorId == activeAccount.userId && newMessage.actorType == "users" + let isActor0Self = collapseByMessage.actorId == self.account.userId && collapseByMessage.actorType == "users" + let isActor1Self = newMessage.actorId == self.account.userId && newMessage.actorType == "users" let isActor0Admin = collapseByMessage.actorId == "cli" && collapseByMessage.actorType == "guests" collapseByMessage.collapsedIncludesUserSelf = isUser0Self || isUser1Self @@ -2438,17 +2582,16 @@ import QuickLook // MARK: - Reactions func addReaction(reaction: String, to message: NCChatMessage) { - if message.reactionsArray().contains(where: {$0.reaction == reaction && $0.userReacted }) { + if message.reactionsArray().contains(where: { $0.reaction == reaction && $0.userReacted }) { // We can't add reaction twice return } - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() self.setTemporaryReaction(reaction: reaction, withState: .adding, toMessage: message) - NCDatabaseManager.sharedInstance().increaseEmojiUsage(forEmoji: reaction, forAccount: activeAccount.accountId) + NCDatabaseManager.sharedInstance().increaseEmojiUsage(forEmoji: reaction, forAccount: self.account.accountId) - NCAPIController.sharedInstance().addReaction(reaction, toMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { _, error, _ in + NCAPIController.sharedInstance().addReaction(reaction, toMessage: message.messageId, inRoom: self.room.token, for: self.account) { _, error, _ in if error != nil { NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while adding a reaction to a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) self.removeTemporaryReaction(reaction: reaction, forMessageId: message.messageId) @@ -2457,10 +2600,9 @@ import QuickLook } func removeReaction(reaction: String, from message: NCChatMessage) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() self.setTemporaryReaction(reaction: reaction, withState: .removing, toMessage: message) - NCAPIController.sharedInstance().removeReaction(reaction, fromMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { _, error, _ in + NCAPIController.sharedInstance().removeReaction(reaction, fromMessage: message.messageId, inRoom: self.room.token, for: self.account) { _, error, _ in if error != nil { NotificationPresenter.shared().present(text: NSLocalizedString("An error occurred while removing a reaction from a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) self.removeTemporaryReaction(reaction: reaction, forMessageId: message.messageId) @@ -2527,8 +2669,7 @@ import QuickLook reactionsVC.room = self.room self.presentWithNavigation(reactionsVC, animated: true) - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().getReactions(nil, fromMessage: message.messageId, inRoom: self.room.token, for: activeAccount) { reactionsDict, error, _ in + NCAPIController.sharedInstance().getReactions(nil, fromMessage: message.messageId, inRoom: self.room.token, for: self.account) { reactionsDict, error, _ in if error == nil, let reactions = reactionsDict as? [String: [[String: AnyObject]]] { @@ -2610,8 +2751,9 @@ import QuickLook guard let message = self.message(for: indexPath) else { continue } DispatchQueue.global(qos: .userInitiated).async { - guard message.messageId != kUnreadMessagesSeparatorIdentifier, - message.messageId != kChatBlockSeparatorIdentifier + guard message.messageId != MessageSeparatorTableViewCell.unreadMessagesSeparatorId, + message.messageId != MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId, + message.messageId != MessageSeparatorTableViewCell.chatBlockSeparatorId else { return } if message.containsURL() { @@ -2632,25 +2774,27 @@ import QuickLook // swiftlint:disable:next cyclomatic_complexity func getCell(for message: NCChatMessage) -> UITableViewCell { - if message.messageId == kUnreadMessagesSeparatorIdentifier, - let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorCellIdentifier) as? MessageSeparatorTableViewCell { + if message.messageId == MessageSeparatorTableViewCell.unreadMessagesSeparatorId || message.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId, + let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorTableViewCell.identifier) as? MessageSeparatorTableViewCell { cell.messageId = message.messageId - cell.separatorLabel.text = NSLocalizedString("Unread messages", comment: "") - return cell - } + cell.separatorLabel.text = MessageSeparatorTableViewCell.unreadMessagesSeparatorText + cell.delegate = self - if message.messageId == kChatBlockSeparatorIdentifier, - let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorCellIdentifier) as? MessageSeparatorTableViewCell { + if message.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId { + cell.setSummaryButtonVisibilty(isHidden: false) + } else { + cell.setSummaryButtonVisibilty(isHidden: true) + } - cell.messageId = message.messageId - cell.separatorLabel.text = NSLocalizedString("Some messages not shown, will be downloaded when online", comment: "") return cell } - if message.isUpdateMessage, - let cell = self.tableView?.dequeueReusableCell(withIdentifier: InvisibleSystemMessageCellIdentifier) as? SystemMessageTableViewCell { + if message.messageId == MessageSeparatorTableViewCell.chatBlockSeparatorId, + let cell = self.tableView?.dequeueReusableCell(withIdentifier: MessageSeparatorTableViewCell.identifier) as? MessageSeparatorTableViewCell { + cell.messageId = message.messageId + cell.separatorLabel.text = MessageSeparatorTableViewCell.chatBlockSeparatorText return cell } @@ -2667,7 +2811,7 @@ import QuickLook if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self - cell.setup(for: message, inRoom: self.room) + cell.setup(for: message, inRoom: self.room, withAccount: self.account) if let playerAudioFileStatus = self.playerAudioFileStatus, let voiceMessagesPlayer = self.voiceMessagesPlayer { @@ -2690,7 +2834,7 @@ import QuickLook if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self - cell.setup(for: message, inRoom: self.room) + cell.setup(for: message, inRoom: self.room, withAccount: self.account) return cell } @@ -2701,7 +2845,7 @@ import QuickLook if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self - cell.setup(for: message, inRoom: self.room) + cell.setup(for: message, inRoom: self.room, withAccount: self.account) return cell } @@ -2712,7 +2856,7 @@ import QuickLook if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self - cell.setup(for: message, inRoom: self.room) + cell.setup(for: message, inRoom: self.room, withAccount: self.account) return cell } @@ -2728,7 +2872,7 @@ import QuickLook if let cell = self.tableView?.dequeueReusableCell(withIdentifier: cellIdentifier) as? BaseChatTableViewCell { cell.delegate = self - cell.setup(for: message, inRoom: self.room) + cell.setup(for: message, inRoom: self.room, withAccount: self.account) return cell } @@ -2764,13 +2908,17 @@ import QuickLook // swiftlint:disable:next cyclomatic_complexity func getCellHeight(for message: NCChatMessage, with originalWidth: CGFloat) -> CGFloat { // Chat separators - if message.messageId == kUnreadMessagesSeparatorIdentifier || - message.messageId == kChatBlockSeparatorIdentifier { - return kMessageSeparatorCellHeight + if message.messageId == MessageSeparatorTableViewCell.unreadMessagesSeparatorId || + message.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId || + message.messageId == MessageSeparatorTableViewCell.chatBlockSeparatorId { + + let cell = self.getCell(for: message) + let size = cell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return size.height } - // Update messages (the ones that notify about an update in one message, they should not be displayed) - if message.message.isEmpty || message.isUpdateMessage || (message.isCollapsed && message.collapsedBy != nil) { + // Empty or collapsed system messages should not be displayed + if message.message.isEmpty || (message.isCollapsed && message.collapsedBy != nil) { return 0.0 } @@ -2823,9 +2971,9 @@ import QuickLook } else if let file = message.file() { if file.previewImageHeight > 0 { height += CGFloat(file.previewImageHeight) - } else if case let estimatedHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedHeight > 0 { - height += estimatedHeight - message.setPreviewImageHeight(estimatedHeight) + } else if case let estimatedSize = BaseChatTableViewCell.getEstimatedPreviewSize(for: message), estimatedSize.height > 0 { + height += estimatedSize.height + message.setPreviewImageSize(estimatedSize) } else { height += fileMessageCellFileMaxPreviewHeight } @@ -2898,7 +3046,11 @@ import QuickLook guard let message = self.message(for: indexPath) else { return nil } - if message.isSystemMessage || message.isDeletedMessage || message.messageId == kUnreadMessagesSeparatorIdentifier { + if message.isSystemMessage || message.isDeletedMessage || + message.messageId == MessageSeparatorTableViewCell.unreadMessagesSeparatorId || + message.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId || + message.messageId == MessageSeparatorTableViewCell.chatBlockSeparatorId { + return nil } @@ -3167,7 +3319,9 @@ import QuickLook } internal func indexPathForUnreadMessageSeparator() -> IndexPath? { - return self.indexPathAndMessageFromEnd(with: { $0.messageId == kUnreadMessagesSeparatorIdentifier })?.indexPath + return self.indexPathAndMessageFromEnd(with: { + $0.messageId == MessageSeparatorTableViewCell.unreadMessagesSeparatorId || $0.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId + })?.indexPath } internal func getLastNonUpdateMessage() -> (indexPath: IndexPath, message: NCChatMessage)? { @@ -3259,7 +3413,7 @@ import QuickLook public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage) { if NCUtils.isImage(fileType: fileParameter.mimetype) { - let mediaViewController = NCMediaViewerViewController(initialMessage: message) + let mediaViewController = NCMediaViewerViewController(initialMessage: message, room: self.room) let navController = CustomPresentableNavigationController(rootViewController: mediaViewController) self.present(navController, interactiveDismissalType: .standard) @@ -3267,6 +3421,20 @@ import QuickLook return } + let filePath = fileParameter.path ?? "" + let fileExtension = URL(fileURLWithPath: filePath).pathExtension.lowercased() + + if NCUtils.isVideo(fileType: fileParameter.mimetype) { + // Skip unsupported formats here ("webm" and "mkv") and use VLC later + if !fileExtension.isEmpty, !VLCKitVideoViewController.supportedFileExtensions.contains(fileExtension) { + let mediaViewController = NCMediaViewerViewController(initialMessage: message, room: self.room) + let navController = CustomPresentableNavigationController(rootViewController: mediaViewController) + + self.present(navController, interactiveDismissalType: .standard) + return + } + } + if fileParameter.fileStatus != nil && fileParameter.fileStatus?.isDownloading ?? false { print("File already downloading -> skipping new download") return @@ -3277,14 +3445,14 @@ import QuickLook downloader.downloadFile(fromMessage: fileParameter) } - public func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage) { - if message.file().previewImageHeight == Int(height) { + public func cellHasDownloadedImagePreview(withSize size: CGSize, for message: NCChatMessage) { + if message.file().previewImageHeight == Int(size.height) { return } let isAtBottom = self.shouldScrollOnNewMessages() - message.setPreviewImageHeight(height) + message.setPreviewImageSize(size) CATransaction.begin() CATransaction.setCompletionBlock { @@ -3372,12 +3540,11 @@ import QuickLook pollVC.room = self.room self.presentWithNavigation(pollVC, animated: true) - if let pollId = Int(poll.parameterId) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().getPollWithId(pollId, inRoom: self.room.token, for: activeAccount) { poll, error, _ in - if error == nil, let poll { - pollVC.updatePoll(poll: poll) - } + guard let pollId = Int(poll.parameterId) else { return } + + NCAPIController.sharedInstance().getPollWithId(pollId, inRoom: self.room.token, for: self.account) { poll, error, _ in + if error == nil, let poll { + pollVC.updatePoll(poll: poll) } } } @@ -3385,8 +3552,7 @@ import QuickLook // MARK: - PollCreationViewControllerDelegate func pollCreationViewControllerWantsToCreatePoll(pollCreationViewController: PollCreationViewController, question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().createPoll(withQuestion: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: self.room.token, for: activeAccount) { _, error, _ in + NCAPIController.sharedInstance().createPoll(withQuestion: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: self.room.token, for: self.account) { _, error, _ in if error != nil { pollCreationViewController.showCreationError() } else { @@ -3444,6 +3610,12 @@ import QuickLook self.didPressReply(for: message) } + // MARK: - MessageSeparatorTableViewCellDelegate + + func generateSummaryButtonPressed() { + // Do nothing -> override in subclass + } + // MARK: - NCChatFileControllerDelegate public func fileControllerDidLoadFile(_ fileController: NCChatFileController, with fileStatus: NCChatFileStatus) { @@ -3557,9 +3729,8 @@ import QuickLook extension Sequence where Iterator.Element == NCChatMessage { - func containsUserMessage() -> Bool { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - return self.contains(where: { !$0.isSystemMessage && $0.actorId == activeAccount.userId }) + func containsMessage(forUserId userId: String) -> Bool { + return self.contains(where: { !$0.isSystemMessage && $0.actorId == userId }) } func containsVisibleMessages() -> Bool { diff --git a/NextcloudTalk/BlurHashDecode.swift b/NextcloudTalk/BlurHashDecode.swift new file mode 100644 index 000000000..13e746f17 --- /dev/null +++ b/NextcloudTalk/BlurHashDecode.swift @@ -0,0 +1,151 @@ +// +// SPDX-FileCopyrightText: 2018 Wolt Enterprises +// SPDX-License-Identifier: MIT +// + +import UIKit + +extension UIImage { + public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { + guard blurHash.count >= 6 else { return nil } + + let sizeFlag = String(blurHash[0]).decode83() + let numY = (sizeFlag / 9) + 1 + let numX = (sizeFlag % 9) + 1 + + let quantisedMaximumValue = String(blurHash[1]).decode83() + let maximumValue = Float(quantisedMaximumValue + 1) / 166 + + guard blurHash.count == 4 + 2 * numX * numY else { return nil } + + let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in + if i == 0 { + let value = String(blurHash[2 ..< 6]).decode83() + return decodeDC(value) + } else { + let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() + return decodeAC(value, maximumValue: maximumValue * punch) + } + } + + let width = Int(size.width) + let height = Int(size.height) + let bytesPerRow = width * 3 + guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } + CFDataSetLength(data, bytesPerRow * height) + guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } + + for y in 0 ..< height { + for x in 0 ..< width { + var r: Float = 0 + var g: Float = 0 + var b: Float = 0 + + for j in 0 ..< numY { + for i in 0 ..< numX { + let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) + let colour = colours[i + j * numX] + r += colour.0 * basis + g += colour.1 * basis + b += colour.2 * basis + } + } + + let intR = UInt8(linearTosRGB(r)) + let intG = UInt8(linearTosRGB(g)) + let intB = UInt8(linearTosRGB(b)) + + pixels[3 * x + 0 + y * bytesPerRow] = intR + pixels[3 * x + 1 + y * bytesPerRow] = intG + pixels[3 * x + 2 + y * bytesPerRow] = intB + } + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) + + guard let provider = CGDataProvider(data: data) else { return nil } + guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } + + self.init(cgImage: cgImage) + } +} + +private func decodeDC(_ value: Int) -> (Float, Float, Float) { + let intR = value >> 16 + let intG = (value >> 8) & 255 + let intB = value & 255 + return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) +} + +private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { + let quantR = value / (19 * 19) + let quantG = (value / 19) % 19 + let quantB = value % 19 + + let rgb = ( + signPow((Float(quantR) - 9) / 9, 2) * maximumValue, + signPow((Float(quantG) - 9) / 9, 2) * maximumValue, + signPow((Float(quantB) - 9) / 9, 2) * maximumValue + ) + + return rgb +} + +private func signPow(_ value: Float, _ exp: Float) -> Float { + return copysign(pow(abs(value), exp), value) +} + +private func linearTosRGB(_ value: Float) -> Int { + let v = max(0, min(1, value)) + if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } + else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } +} + +private func sRGBToLinear(_ value: Type) -> Float { + let v = Float(Int64(value)) / 255 + if v <= 0.04045 { return v / 12.92 } + else { return pow((v + 0.055) / 1.055, 2.4) } +} + +private let encodeCharacters: [String] = { + return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } +}() + +private let decodeCharacters: [String: Int] = { + var dict: [String: Int] = [:] + for (index, character) in encodeCharacters.enumerated() { + dict[character] = index + } + return dict +}() + +extension String { + func decode83() -> Int { + var value: Int = 0 + for character in self { + if let digit = decodeCharacters[String(character)] { + value = value * 83 + digit + } + } + return value + } +} + +private extension String { + subscript (offset: Int) -> Character { + return self[index(startIndex, offsetBy: offset)] + } + + subscript (bounds: CountableClosedRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start...end] + } + + subscript (bounds: CountableRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start.. no need to keep ringing + [self endCallWithUUID:call.uuid]; + return; + } + + // Reschedule next check + NSTimer *callStateTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerCheckCallStateEverySeconds target:self selector:@selector(checkCallStateForTimer:) userInfo:call repeats:NO]; + [weakSelf.callStateTimers setObject:callStateTimer forKey:call.uuid]; + }]; +} + +- (void)checkCallStateWithPeersForCall:(CallKitCall *)call +{ + __weak CallKitManager *weakSelf = self; + TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:call.accountId]; [[NCAPIController sharedInstance] getPeersForCall:call.token forAccount:account withCompletionBlock:^(NSMutableArray *peers, NSError *error, NSInteger statusCode) { // Make sure call is still ringing at this point to avoid a race-condition between answering the call on this device and the API callback @@ -386,7 +429,7 @@ - (void)checkCallStateForCall:(NSTimer *)timer [self endCallWithUUID:call.uuid]; return; } - + NSInteger callAPIVersion = [[NCAPIController sharedInstance] callAPIVersionForAccount:account]; for (NSMutableDictionary *user in peers) { NSString *userId = [user objectForKey:@"userId"]; @@ -401,9 +444,9 @@ - (void)checkCallStateForCall:(NSTimer *)timer return; } } - + // Reschedule next check - NSTimer *callStateTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerCheckCallStateEverySeconds target:self selector:@selector(checkCallStateForCall:) userInfo:call repeats:NO]; + NSTimer *callStateTimer = [NSTimer scheduledTimerWithTimeInterval:kCallKitManagerCheckCallStateEverySeconds target:self selector:@selector(checkCallStateForTimer:) userInfo:call repeats:NO]; [weakSelf.callStateTimers setObject:callStateTimer forKey:call.uuid]; }]; } diff --git a/NextcloudTalk/CallParticipantViewCell.m b/NextcloudTalk/CallParticipantViewCell.m index df676e27d..fa92f36cd 100644 --- a/NextcloudTalk/CallParticipantViewCell.m +++ b/NextcloudTalk/CallParticipantViewCell.m @@ -124,7 +124,8 @@ - (void)setAvatarForActor:(TalkActor * _Nullable)actor [self setBackgroundColor:[[ColorGenerator shared] usernameToColor:actor.id]]; } - [self.peerAvatarImageView setActorAvatarForId:actor.id withType:actor.type withDisplayName:actor.displayName withRoomToken:nil]; + TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; + [self.peerAvatarImageView setActorAvatarForId:actor.id withType:actor.type withDisplayName:actor.displayName withRoomToken:nil using:activeAccount]; } - (void)setDisplayName:(NSString *)displayName diff --git a/NextcloudTalk/CallViewController.swift b/NextcloudTalk/CallViewController.swift index 7ca8e5b7e..9efd5bf9d 100644 --- a/NextcloudTalk/CallViewController.swift +++ b/NextcloudTalk/CallViewController.swift @@ -259,13 +259,12 @@ class CallViewController: UIViewController, self.userDisabledSpeaker = true } - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() // 'conversation-permissions' capability was not added in Talk 13 release, so we check for 'direct-mention-flag' capability // as a workaround. let serverSupportsConversationPermissions = - NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationPermissions, forAccountId: activeAccount.accountId) || - NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityDirectMentionFlag, forAccountId: activeAccount.accountId) + NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationPermissions, forAccountId: room.accountId) || + NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityDirectMentionFlag, forAccountId: room.accountId) if serverSupportsConversationPermissions { self.setAudioMuteButtonEnabled(room.permissions.contains(.canPublishAudio)) @@ -278,7 +277,7 @@ class CallViewController: UIViewController, NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange(notification:)), name: UIDevice.proximityStateDidChangeNotification, object: nil) // callStartTime is only available if we have the "recording-v1" capability - if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityRecordingV1, forAccountId: activeAccount.accountId) { + if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityRecordingV1, forAccountId: room.accountId) { self.callDurationTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(callDurationTimerUpdate), userInfo: nil, repeats: true) } } @@ -829,9 +828,9 @@ class CallViewController: UIViewController, } // Connect to new call - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() NCRoomsManager.sharedInstance().updateRoom(token) { roomDict, error in - guard error == nil else { + guard error == nil, let newRoom = NCRoom(dictionary: roomDict, andAccountId: self.room.accountId) + else { print("Error getting room to switch") return } @@ -842,7 +841,7 @@ class CallViewController: UIViewController, self.delegate?.callViewController(self, wantsToSwitchFromCall: self.room.token, toRoom: token) // Assign new room as current room - self.room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) + self.room = newRoom // Save current audio and video state self.audioDisabledAtStart = !audioEnabled @@ -1296,8 +1295,7 @@ class CallViewController: UIViewController, } // Send a reaction - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) + let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: room.accountId) // Disable swiftlint -> not supported on Realm object // swiftlint:disable:next empty_count @@ -1307,7 +1305,10 @@ class CallViewController: UIViewController, for reaction in callReactions { reactionItems.append(UIAction(title: String(reaction), handler: { [unowned self] _ in callController.sendReaction(reaction) - self.addReaction(reaction, fromUser: activeAccount.userDisplayName) + + if let account = room.account { + self.addReaction(reaction, fromUser: account.userDisplayName) + } })) } @@ -1905,10 +1906,9 @@ class CallViewController: UIViewController, func showChat() { if chatNavigationController == nil { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - - guard let room = NCDatabaseManager.sharedInstance().room(withToken: room.token, forAccountId: activeAccount.accountId), - let chatViewController = ChatViewController(for: room) + guard let room = NCDatabaseManager.sharedInstance().room(withToken: room.token, forAccountId: room.accountId), + let account = room.account, + let chatViewController = ChatViewController(forRoom: room, withAccount: account) else { return } chatViewController.presentedInCall = true diff --git a/NextcloudTalk/ChatViewController.swift b/NextcloudTalk/ChatViewController.swift index 55ea7ee76..85efef3bb 100644 --- a/NextcloudTalk/ChatViewController.swift +++ b/NextcloudTalk/ChatViewController.swift @@ -7,6 +7,7 @@ import Foundation import NextcloudKit import PhotosUI import UIKit +import SwiftyAttributes @objcMembers public class ChatViewController: BaseChatViewController { @@ -26,12 +27,25 @@ import UIKit private var offlineMode = false private var hasStoredHistory = true private var hasStopped = false + private var hasCheckedOutOfOfficeStatus = false private var chatViewPresentedTimestamp = Date().timeIntervalSince1970 + private var generateSummaryFromMessageId: Int? + private var generateSummaryTimer: Timer? private lazy var unreadMessagesSeparator: NCChatMessage = { let message = NCChatMessage() - message.messageId = kUnreadMessagesSeparatorIdentifier + + message.messageId = MessageSeparatorTableViewCell.unreadMessagesSeparatorId + + // We decide at this point if the unread marker should be with/without summary button, so it doesn't get changed when the room is updated + if !self.room.isFederated, NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityChatSummary, forAccountId: self.room.accountId), + let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.room.accountId), + serverCapabilities.summaryThreshold <= self.room.unreadMessages { + + message.messageId = MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId + } + return message }() @@ -106,10 +120,10 @@ import UIKit private var messageExpirationTimer: Timer? - public override init?(for room: NCRoom) { + public override init?(forRoom room: NCRoom, withAccount account: TalkAccount) { self.chatController = NCChatController(for: room) - super.init(for: room) + super.init(forRoom: room, withAccount: account) NotificationCenter.default.addObserver(self, selector: #selector(didUpdateRoom(notification:)), name: NSNotification.Name.NCRoomsManagerDidUpdateRoom, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didJoinRoom(notification:)), name: NSNotification.Name.NCRoomsManagerDidJoinRoom, object: nil) @@ -175,6 +189,7 @@ import UIKit self.checkLobbyState() self.checkRoomControlsAvailability() + self.checkOutOfOfficeAbsence() self.startObservingExpiredMessages() @@ -187,44 +202,13 @@ import UIKit } if !self.offlineMode { - if self.room.token == nil { - fatalTokenError() - } - NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false) } - } - private func fatalTokenError() { - let capabilities = NCDatabaseManager.sharedInstance().serverCapabilities() - - switch capabilities.versionMajor { - case 19: - fatalError() - case 20: - fatalError() - case 21: - fatalError() - case 22: - fatalError() - case 23: - fatalError() - case 24: - fatalError() - case 25: - fatalError() - case 26: - fatalError() - case 27: - fatalError() - case 28: - fatalError() - case 29: - fatalError() - case 30: - fatalError() - default: - fatalError() + // Check if there are summary tasks still running, but not yet finished + if !AiSummaryController.shared.getSummaryTaskIds(forRoomInternalId: self.room.internalId).isEmpty { + self.showGeneratingSummaryNotification() + self.scheduleSummaryTaskCheck() } } @@ -268,11 +252,7 @@ import UIKit // Check if new messages were added while the app was inactive (eg. via background-refresh) self.checkForNewStoredMessages() - if !self.offlineMode { - if self.room.token == nil { - fatalTokenError() - } - + if !self.offlineMode { NCRoomsManager.sharedInstance().joinRoom(self.room.token, forCall: false) } @@ -470,6 +450,38 @@ import UIKit self.checkRoomControlsAvailability() } + let outOfOfficeView: OutOfOfficeView? = nil + + func checkOutOfOfficeAbsence() { + // Only check once, and only for 1:1 on DND right now + guard self.hasCheckedOutOfOfficeStatus == false, + self.room.type == .oneToOne, + self.room.status == kUserStatusDND + else { return } + + self.hasCheckedOutOfOfficeStatus = true + + NCAPIController.sharedInstance().getUserAbsence(forAccountId: self.room.accountId, forUserId: self.room.name) { absenceData in + guard let absenceData else { return } + + let oooView = OutOfOfficeView() + oooView.setupAbsence(withData: absenceData, inRoom: self.room) + oooView.alpha = 0 + + self.view.addSubview(oooView) + + NSLayoutConstraint.activate([ + oooView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor), + oooView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor), + oooView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor) + ]) + + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) { + oooView.alpha = 1.0 + } + } + } + // MARK: - Message expiration func startObservingExpiredMessages() { @@ -527,7 +539,8 @@ import UIKit override func sendChatMessage(message: String, withParentMessage parentMessage: NCChatMessage?, messageParameters: String, silently: Bool) { // Create temporary message - let temporaryMessage = self.createTemporaryMessage(message: message,replyTo: parentMessage,messageParameters: messageParameters, silently: silently, isVoiceMessage: false) + guard let temporaryMessage = self.createTemporaryMessage(message: message, replyTo: parentMessage, messageParameters: messageParameters, silently: silently, isVoiceMessage: false) + else { return } if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReferenceId, for: room) { self.appendTemporaryMessage(temporaryMessage: temporaryMessage) @@ -645,10 +658,16 @@ import UIKit } public func leaveChat() { + self.hasStopped = true self.lobbyCheckTimer?.invalidate() self.messageExpirationTimer?.invalidate() + self.generateSummaryTimer?.invalidate() self.chatController.stop() + // Dismiss possible notifications + // swiftlint:disable:next notification_center_detachment + NotificationCenter.default.removeObserver(self) + // In case we're typing when we leave the chat, make sure we notify everyone // The 'stopTyping' method makes sure to only send signaling messages when we were typing before self.stopTyping(force: false) @@ -725,7 +744,7 @@ import UIKit return } - if let room = notification.userInfo?["room"] as? NCRoom { + if let room = notification.userInfo?["room"] as? NCRoom, room.token == self.room.token { self.room = room } @@ -819,6 +838,8 @@ import UIKit continue } + // Store the messageId separately from self.lastReadMessage as that might change during a room update + self.generateSummaryFromMessageId = message.messageId messages.insert(self.unreadMessagesSeparator, at: messageIndex + 1) self.messages[dateSection] = messages indexPathUnreadMessageSeparator = IndexPath(row: messageIndex + 1, section: sectionIndex) @@ -955,9 +976,11 @@ import UIKit var addedUnreadMessageSeparator = false // Check if unread messages separator should be added (only if it's not already shown) - if firstNewMessagesAfterHistory, self.getLastRealMessage() != nil, self.indexPathForUnreadMessageSeparator() == nil, newMessagesContainVisibleMessages, + if firstNewMessagesAfterHistory, let lastRealMessage = self.getLastRealMessage(), self.indexPathForUnreadMessageSeparator() == nil, newMessagesContainVisibleMessages, let lastDateSection = self.dateSections.last, var messagesBeforeUpdate = self.messages[lastDateSection] { + // Store the messageId separately from self.lastReadMessage as that might change during a room update + self.generateSummaryFromMessageId = lastRealMessage.message.messageId messagesBeforeUpdate.append(self.unreadMessagesSeparator) self.messages[lastDateSection] = messagesBeforeUpdate insertIndexPaths.insert(IndexPath(row: messagesBeforeUpdate.count - 1, section: self.dateSections.count - 1)) @@ -967,6 +990,18 @@ import UIKit self.appendMessages(messages: messages) for newMessage in messages { + // Update messages might trigger an reload of another cell, but are not part of the tableView itself + if newMessage.isUpdateMessage { + if let parentMessage = newMessage.parent, let parentPath = self.indexPath(for: parentMessage) { + if parentPath.section < tableView.numberOfSections, parentPath.row < tableView.numberOfRows(inSection: parentPath.section) { + // We received an update message to a message which is already part of our current data, therefore we need to reload it + reloadIndexPaths.insert(parentPath) + } + } + + continue + } + // If we don't get an indexPath here, something is wrong with our appendMessages function let indexPath = self.indexPath(for: newMessage)! @@ -983,13 +1018,6 @@ import UIKit insertIndexPaths.insert(indexPath) } - if newMessage.isUpdateMessage, let parentMessage = newMessage.parent, let parentPath = self.indexPath(for: parentMessage) { - if parentPath.section < tableView.numberOfSections, parentPath.row < tableView.numberOfRows(inSection: parentPath.section) { - // We received an update message to a message which is already part of our current data, therefore we need to reload it - reloadIndexPaths.insert(parentPath) - } - } - if let collapsedByMessage = newMessage.collapsedBy, let collapsedPath = self.indexPath(for: collapsedByMessage) { if collapsedPath.section < tableView.numberOfSections, collapsedPath.row < tableView.numberOfRows(inSection: collapsedPath.section) { // The current message is collapsed, so we need to make sure that the collapsedBy message is reloaded @@ -1013,7 +1041,7 @@ import UIKit } completion: { _ in // Remove unread messages separator when user writes a message - if messages.containsUserMessage() { + if messages.containsMessage(forUserId: self.account.userId) { self.removeUnreadMessagesSeparator() } @@ -1021,7 +1049,7 @@ import UIKit // Otherwise we would scroll whenever a unread message separator is available if addedUnreadMessageSeparator, let indexPathUnreadMessageSeparator = self.indexPathForUnreadMessageSeparator() { tableView.scrollToRow(at: indexPathUnreadMessageSeparator, at: .middle, animated: true) - } else if (shouldScrollOnNewMessages || messages.containsUserMessage()), let lastIndexPath = self.getLastRealMessage()?.indexPath { + } else if (shouldScrollOnNewMessages || messages.containsMessage(forUserId: self.account.userId)), let lastIndexPath = self.getLastRealMessage()?.indexPath { tableView.scrollToRow(at: lastIndexPath, at: .none, animated: true) } else if self.firstUnreadMessage == nil, newMessagesContainVisibleMessages, let firstNewMessage = messages.first { // This check is needed since several calls to receiveMessages API might be needed @@ -1192,11 +1220,9 @@ import UIKit guard serverSupportsConversationPermissions else { return } - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - // Retrieve the information about ourselves guard let userDict = notification.userInfo?["users"] as? [[String: String]], - let appUserDict = userDict.first(where: { $0["userId"] == activeAccount.userId }) + let appUserDict = userDict.first(where: { $0["userId"] == self.account.userId }) else { return } // Check if we still have the same permissions @@ -1225,12 +1251,11 @@ import UIKit guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: self.room.accountId) else { return } - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() let userId = notification.userInfo?["userId"] as? String let isFederated = notification.userInfo?["isFederated"] as? Bool ?? false // Since our own userId can exist on other servers, only suppress the notification if it's not federated - if (userId == activeAccount.userId && !isFederated) || serverCapabilities.typingPrivacy { + if (userId == self.account.userId && !isFederated) || serverCapabilities.typingPrivacy { return } @@ -1359,13 +1384,11 @@ import UIKit public override func didCommitTextEditing(_ sender: Any) { if let editingMessage { - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - let messageParametersJSONString = NCMessageParameter.messageParametersJSONString(from: self.mentionsDict) ?? "" editingMessage.message = self.replaceMentionsDisplayNamesWithMentionsKeysInMessage(message: self.textView.text, parameters: messageParametersJSONString) editingMessage.messageParametersJSONString = messageParametersJSONString - NCAPIController.sharedInstance().editChatMessage(inRoom: editingMessage.token, withMessageId: editingMessage.messageId, withMessage: editingMessage.sendingMessage, for: activeAccount) { messageDict, error, _ in + NCAPIController.sharedInstance().editChatMessage(inRoom: editingMessage.token, withMessageId: editingMessage.messageId, withMessage: editingMessage.sendingMessage, for: account) { messageDict, error, _ in if error != nil { NotificationPresenter.shared().present(text: NSLocalizedString("Error occurred while editing a message", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error) return @@ -1373,7 +1396,7 @@ import UIKit guard let messageDict, let parent = messageDict["parent"] as? [AnyHashable: Any], - let updatedMessage = NCChatMessage(dictionary: parent, andAccountId: activeAccount.accountId) + let updatedMessage = NCChatMessage(dictionary: parent, andAccountId: self.account.accountId) else { return } self.updateMessage(withMessageId: editingMessage.messageId, updatedMessage: updatedMessage) @@ -1389,6 +1412,95 @@ import UIKit self.addOrRemoveReaction(reaction: reaction, in: message) } + // MARK: - MessageSeparatorTableViewCellDelegate + + override func generateSummaryButtonPressed() { + guard self.indexPathForUnreadMessageSeparator() != nil, let generateSummaryFromMessageId else { return } + + self.generateSummary(fromMessageId: generateSummaryFromMessageId) + self.showGeneratingSummaryNotification() + } + + func showGeneratingSummaryNotification() { + NotificationPresenter.shared().present(title: NSLocalizedString("Generating summary of unread messages", comment: ""), subtitle: NSLocalizedString("This might take a moment", comment: ""), includedStyle: .dark) + NotificationPresenter.shared().displayActivityIndicator(true) + } + + func generateSummary(fromMessageId messageId: Int) { + NCAPIController.sharedInstance().summarizeChat(forAccountId: self.room.accountId, inRoom: self.room.token, fromMessageId: messageId) { status, taskId, nextOffset in + if status == .noAiProvider { + NotificationPresenter.shared().present(text: NSLocalizedString("No AI provider available or summarizing failed", comment: ""), dismissAfterDelay: 7.0, includedStyle: .error) + return + } + + if status == .noMessagesFound { + NotificationPresenter.shared().present(text: NSLocalizedString("No messages found to summarize", comment: ""), dismissAfterDelay: 7.0, includedStyle: .error) + return + } + + guard let taskId, status != .failed else { + NotificationPresenter.shared().present(text: NSLocalizedString("Generating summary of unread messages failed", comment: ""), dismissAfterDelay: 7.0, includedStyle: .error) + return + } + + AiSummaryController.shared.addSummaryTaskId(forRoomInternalId: self.room.internalId, withTaskId: taskId) + + print("Scheduled summary task with taskId \(taskId) and nextOffset \(String(describing: nextOffset))") + + // Add a safe-guard to make sure there's really a nextOffset. Otherwise we might end up requesting the same task over and over again + if let nextOffset, nextOffset > messageId { + // We were not able to get a summary of all messages at once, so we need to create another summary task + self.generateSummary(fromMessageId: nextOffset) + } else { + // There's no offset anymore (or there never was one) so we start checking the task states + self.scheduleSummaryTaskCheck() + } + } + } + + func scheduleSummaryTaskCheck() { + self.generateSummaryTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard + let self, + let firstTaskId = AiSummaryController.shared.getSummaryTaskIds(forRoomInternalId: self.room.internalId).first + else { return } + + NCAPIController.sharedInstance().getAiTaskById(for: self.room.accountId, withTaskId: firstTaskId) { [weak self] status, output in + guard let self else { return } + + if status == .successful { + let resultOutput = output ?? NSLocalizedString("Empty summary response", comment: "") + AiSummaryController.shared.markSummaryTaskAsDone(forRoomInternalId: self.room.internalId, withTaskId: firstTaskId, withOutput: resultOutput) + + if AiSummaryController.shared.getSummaryTaskIds(forRoomInternalId: self.room.internalId).isEmpty { + // No more taskIds to check -> show the summary + NotificationPresenter.shared().dismiss() + + let outputs = AiSummaryController.shared.finalizeSummaryTask(forRoomInternalId: self.room.internalId) + let summaryVC = AiSummaryViewController(summaryText: outputs.joined(separator: "\n\n---\n\n")) + let navController = UINavigationController(rootViewController: summaryVC) + self.present(navController, animated: true) + + return + } + + } else if status == .failed { + AiSummaryController.shared.finalizeSummaryTask(forRoomInternalId: self.room.internalId) + NotificationPresenter.shared().dismiss() + NotificationPresenter.shared().present(text: NSLocalizedString("Generating summary of unread messages failed", comment: ""), dismissAfterDelay: 7.0, includedStyle: .error) + + return + } else if status == .cancelled { + AiSummaryController.shared.finalizeSummaryTask(forRoomInternalId: self.room.internalId) + NotificationPresenter.shared().dismiss() + return + } + + self.scheduleSummaryTaskCheck() + } + }) + } + // MARK: - ContextMenu (Long press on message) func isMessageReplyable(message: NCChatMessage) -> Bool { @@ -1609,15 +1721,19 @@ import UIKit } } - guard let message = self.message(for: indexPath) else { return nil } + guard let message = self.message(for: indexPath) + else { return nil } + + if message.isSystemMessage || message.isDeletedMessage || + message.messageId == MessageSeparatorTableViewCell.unreadMessagesSeparatorId || + message.messageId == MessageSeparatorTableViewCell.unreadMessagesWithSummarySeparatorId || + message.messageId == MessageSeparatorTableViewCell.chatBlockSeparatorId { - if message.isSystemMessage || message.isDeletedMessage || message.messageId == kUnreadMessagesSeparatorIdentifier { return nil } var actions: [UIMenuElement] = [] var informationalActions: [UIMenuElement] = [] - let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || self.room.permissions.contains(.chat) // Show edit information @@ -1657,7 +1773,7 @@ import UIKit } // Reply-privately option (only to other users and not in one-to-one) - if self.isMessageReplyable(message: message), self.room.type != .oneToOne, message.actorType == "users", message.actorId != activeAccount.userId { + if self.isMessageReplyable(message: message), self.room.type != .oneToOne, message.actorType == "users", message.actorId != self.account.userId { actions.append(UIAction(title: NSLocalizedString("Reply privately", comment: ""), image: .init(systemName: "person")) { _ in self.didPressReplyPrivately(for: message) }) @@ -1732,7 +1848,7 @@ import UIKit }) // Translate - if !self.offlineMode, NCDatabaseManager.sharedInstance().hasAvailableTranslations(forAccountId: activeAccount.accountId) { + if !self.offlineMode, NCDatabaseManager.sharedInstance().hasAvailableTranslations(forAccountId: self.account.accountId) { actions.append(UIAction(title: NSLocalizedString("Translate", comment: ""), image: .init(systemName: "character.book.closed")) { _ in self.didPressTranslate(for: message) }) @@ -1757,14 +1873,14 @@ import UIKit var destructiveMenuActions: [UIMenuElement] = [] // Edit option - if message.isEditable(for: activeAccount, in: self.room) && hasChatPermissions { + if message.isEditable(for: self.account, in: self.room) && hasChatPermissions { destructiveMenuActions.append(UIAction(title: NSLocalizedString("Edit", comment: "Edit a message or room participants"), image: .init(systemName: "pencil")) { _ in self.didPressEdit(for: message) }) } // Delete option - if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: activeAccount, in: self.room) && hasChatPermissions) { + if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: self.account, in: self.room) && hasChatPermissions) { destructiveMenuActions.append(UIAction(title: NSLocalizedString("Delete", comment: ""), image: .init(systemName: "trash"), attributes: .destructive) { _ in self.didPressDelete(for: message) }) diff --git a/NextcloudTalk/ContactsSearchResultTableViewContoller.swift b/NextcloudTalk/ContactsSearchResultTableViewContoller.swift index 1279f6b89..82fc97135 100644 --- a/NextcloudTalk/ContactsSearchResultTableViewContoller.swift +++ b/NextcloudTalk/ContactsSearchResultTableViewContoller.swift @@ -86,7 +86,8 @@ import UIKit contactCell.labelTitle.text = contact.name let contactType = contact.source as String - contactCell.contactImage.setActorAvatar(forId: contact.userId, withType: contactType, withDisplayName: contact.name, withRoomToken: nil) + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + contactCell.contactImage.setActorAvatar(forId: contact.userId, withType: contactType, withDisplayName: contact.name, withRoomToken: nil, using: activeAccount) return contactCell } diff --git a/NextcloudTalk/ContextChatViewController.swift b/NextcloudTalk/ContextChatViewController.swift index 3145fc078..698b3399c 100644 --- a/NextcloudTalk/ContextChatViewController.swift +++ b/NextcloudTalk/ContextChatViewController.swift @@ -7,8 +7,8 @@ import Foundation @objcMembers public class ContextChatViewController: BaseChatViewController { - public override func viewDidLoad() { - super.viewDidLoad() + override func setTitleView() { + super.setTitleView() self.titleView?.longPressGestureRecognizer.isEnabled = false } diff --git a/NextcloudTalk/DiagnosticsTableViewController.swift b/NextcloudTalk/DiagnosticsTableViewController.swift index 1048b3b29..312294c9f 100644 --- a/NextcloudTalk/DiagnosticsTableViewController.swift +++ b/NextcloudTalk/DiagnosticsTableViewController.swift @@ -34,10 +34,10 @@ class DiagnosticsTableViewController: UITableViewController { } enum AccountSections: Int { - case kAccountSectionServer = 0 - case kAccountSectionUser - case kAccountPushSubscribed - case kAccountSectionCount + case server = 0 + case user + case pushSubscribed + case testPushNotifications } enum ServerSections: Int { @@ -79,6 +79,8 @@ class DiagnosticsTableViewController: UITableViewController { var serverReachable: Bool? var serverReachableIndicator = UIActivityIndicatorView(frame: .init(x: 0, y: 0, width: 24, height: 24)) + var testingPushNotificationsIndicator = UIActivityIndicatorView(frame: .init(x: 0, y: 0, width: 24, height: 24)) + var notificationSettings: UNNotificationSettings? var notificationSettingsIndicator = UIActivityIndicatorView(frame: .init(x: 0, y: 0, width: 24, height: 24)) @@ -87,7 +89,7 @@ class DiagnosticsTableViewController: UITableViewController { let notRequestedString = NSLocalizedString("Not requested", comment: "'{Microphone, Camera, ...} access was not requested'") let deniedFunctionalityString = NSLocalizedString("This will impact the functionality of this app. Please review your settings.", comment: "") - let cellIdentifierOpenAppSettings = "cellIdentifierOpenAppSettings" + let cellIdentifierAction = "cellIdentifierAction" let cellIdentifierSubtitle = "cellIdentifierSubtitle" let cellIdentifierSubtitleAccessory = "cellIdentifierSubtitleAccessory" @@ -133,13 +135,25 @@ class DiagnosticsTableViewController: UITableViewController { self.navigationItem.compactAppearance = appearance self.navigationItem.scrollEdgeAppearance = appearance - self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifierOpenAppSettings) + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifierAction) self.tableView.register(SubtitleTableViewCell.self, forCellReuseIdentifier: cellIdentifierSubtitle) self.tableView.register(SubtitleTableViewCell.self, forCellReuseIdentifier: cellIdentifierSubtitleAccessory) runChecks() } + // MARK: - Account section options + + func accountSections() -> [AccountSections] { + var sections: [AccountSections] = [.server, .user, .pushSubscribed] + if NCDatabaseManager.sharedInstance().serverHasNotificationsCapability(kNotificationsCapabilityTestPush, forAccountId: account.accountId) { + sections.append(.testPushNotifications) + } + + return sections + } + + // MARK: Async. checks func runChecks() { @@ -191,7 +205,7 @@ class DiagnosticsTableViewController: UITableViewController { return AppSections.kAppSectionCount.rawValue case DiagnosticsSections.kDiagnosticsSectionAccount.rawValue: - return AccountSections.kAccountSectionCount.rawValue + return accountSections().count case DiagnosticsSections.kDiagnosticsSectionServer.rawValue: return ServerSections.kServerSectionCount.rawValue @@ -259,6 +273,11 @@ class DiagnosticsTableViewController: UITableViewController { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } else if indexPath.section == DiagnosticsSections.kDiagnosticsSectionAccount.rawValue, + accountSections()[indexPath.row] == .testPushNotifications { + + testPushNotifications() + } else if indexPath.section == DiagnosticsSections.kDiagnosticsSectionTalk.rawValue, indexPath.row == TalkSections.kTalkSectionVersion.rawValue { @@ -307,7 +326,7 @@ class DiagnosticsTableViewController: UITableViewController { return appPhotoLibraryAccessCell(for: indexPath) case AppSections.kAppSectionOpenSettings.rawValue: - let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierOpenAppSettings, for: indexPath) + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierAction, for: indexPath) cell.textLabel?.text = NSLocalizedString("Open app settings", comment: "") cell.textLabel?.textAlignment = .center @@ -452,17 +471,18 @@ class DiagnosticsTableViewController: UITableViewController { func accountCell(for indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierSubtitle, for: indexPath) + let row: AccountSections = accountSections()[indexPath.row] - switch indexPath.row { - case AccountSections.kAccountSectionServer.rawValue: + switch row { + case .server: cell.textLabel?.text = NSLocalizedString("Server", comment: "") cell.detailTextLabel?.text = account.server - case AccountSections.kAccountSectionUser.rawValue: + case .user: cell.textLabel?.text = NSLocalizedString("User", comment: "") cell.detailTextLabel?.text = account.user - case AccountSections.kAccountPushSubscribed.rawValue: + case .pushSubscribed: cell.textLabel?.text = NSLocalizedString("Push notifications", comment: "") if account.lastPushSubscription > 0 { let lastSubsctiptionString = NSLocalizedString("Last subscription", comment: "Last subscription to the push notification server") @@ -472,8 +492,12 @@ class DiagnosticsTableViewController: UITableViewController { cell.detailTextLabel?.text = NSLocalizedString("Never subscribed", comment: "Never subscribed to the push notification server") } - default: - break + case .testPushNotifications: + let actionCell = self.tableView.dequeueReusableCell(withIdentifier: self.cellIdentifierAction, for: indexPath) + actionCell.textLabel?.text = NSLocalizedString("Test push notifications", comment: "") + actionCell.textLabel?.textAlignment = .center + actionCell.textLabel?.textColor = UIColor.systemBlue + return actionCell } return cell @@ -623,6 +647,57 @@ class DiagnosticsTableViewController: UITableViewController { return cell } + // MARK: Test push notifications + + func testPushNotifications() { + self.showPushNotificationTestRunningIndicator() + NCAPIController.sharedInstance().testPushnotifications(forAccount: account) { result in + let isEmptyResult = result?.isEmpty ?? true + let title = isEmptyResult ? NSLocalizedString("Test failed", comment: "") : NSLocalizedString("Test results", comment: "") + let message = isEmptyResult ? NSLocalizedString("An error occurred while testing push notifications", comment: "") : result + let alert = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default)) + if !isEmptyResult { + alert.addAction(UIAlertAction(title: NSLocalizedString("Copy", comment: ""), style: .default) { _ in + UIPasteboard.general.string = result + NotificationPresenter.shared().present(text: NSLocalizedString("Test results copied", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark) + }) + } + NCUserInterfaceController.sharedInstance().presentAlertViewController(alert) + self.hidePushNotificationTestRunningIndicator() + } + } + + func testPushNotificationsCell() -> UITableViewCell? { + if let index = accountSections().firstIndex(of: AccountSections.testPushNotifications), + let testPushCell = tableView.cellForRow(at: IndexPath(row: index, section: DiagnosticsSections.kDiagnosticsSectionAccount.rawValue)) { + return testPushCell + } + + return nil + } + + func showPushNotificationTestRunningIndicator() { + if let testPushNotificationsCell = testPushNotificationsCell() { + testPushNotificationsCell.isUserInteractionEnabled = false + testPushNotificationsCell.textLabel?.textColor = UIColor.systemBlue.withAlphaComponent(0.5) + testingPushNotificationsIndicator.startAnimating() + testPushNotificationsCell.accessoryView = testingPushNotificationsIndicator + } + } + + func hidePushNotificationTestRunningIndicator() { + if let testPushNotificationsCell = testPushNotificationsCell() { + testPushNotificationsCell.isUserInteractionEnabled = true + testPushNotificationsCell.textLabel?.textColor = UIColor.systemBlue + testingPushNotificationsIndicator.stopAnimating() + testPushNotificationsCell.accessoryView = nil + } + } + // MARK: Capabilities details func presentCapabilitiesDetails() { diff --git a/NextcloudTalk/ExpandedVoiceMessageRecordingView.swift b/NextcloudTalk/ExpandedVoiceMessageRecordingView.swift new file mode 100644 index 000000000..ea9ba27ad --- /dev/null +++ b/NextcloudTalk/ExpandedVoiceMessageRecordingView.swift @@ -0,0 +1,92 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import SwiftUI + +func formatSeconds(seconds: Int) -> String { + let minutes = seconds / 60 + let seconds = seconds % 60 + return String(format: "%02d:%02d", minutes, seconds) + +} + +struct ExpandedVoiceMessageRecordingView: View { + var buttonPadding: CGFloat = 40 + var deleteFunc: () -> Void + var sendFunc: () -> Void + var recordFunc: (Bool) -> Void + + @State var isRecording = true + @State var timeElapsed: Int + @State var timeFormatted = "" + @State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + VStack { + Text("\(timeFormatted)") + .font(.largeTitle) + .bold() + .padding(.trailing, 10) + .frame(alignment: .center) + .border(.clear) + .onReceive(timer) { _ in + if isRecording { + timeElapsed += 1 + timeFormatted = formatSeconds(seconds: timeElapsed) + } + } + HStack { + Button(action: { // Delete Recording + self.deleteFunc() + }, label: { + Label("", systemImage: "trash").font(.title2) + }) + Spacer() + Button(action: { // End/Restart Recording + isRecording.toggle() + + if isRecording { + timeElapsed = 0 + timeFormatted = formatSeconds(seconds: 0) + timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + } + + self.recordFunc(isRecording) + + }, label: { + Label("", systemImage: isRecording ? "square.fill" : "arrow.clockwise.square").font(.title2) + }) + Spacer() + Button(action: { // Send Recording + self.sendFunc() + + }, label: { + Label("", systemImage: "paperplane").font(.title2) + }) + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, buttonPadding) + .padding(.bottom, 10) + .border(.clear) + + } + .frame(maxWidth: .infinity) + .border(.clear) + .background(Color(NCAppBranding.backgroundColor())) + .onAppear { + timeFormatted = formatSeconds(seconds: timeElapsed) + } + } +} + +//#Preview { +// ExpandedVoiceMessageRecordingView(deleteFunc: { +// // unused atm +// }, sendFunc: { +// // unused atm +// }, recordFunc: { _ in +// // unused atm +// }, timeElapsed: 0) +//} diff --git a/NextcloudTalk/FederationInvitationTableViewController.swift b/NextcloudTalk/FederationInvitationTableViewController.swift index 03ae66a1e..06705eebc 100644 --- a/NextcloudTalk/FederationInvitationTableViewController.swift +++ b/NextcloudTalk/FederationInvitationTableViewController.swift @@ -40,7 +40,7 @@ class FederationInvitationTableViewController: UITableViewController, Federation barButtonItem.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { [unowned self] _ in self.dismiss(animated: true) }) - self.navigationItem.rightBarButtonItems = [barButtonItem] + self.navigationItem.leftBarButtonItems = [barButtonItem] self.tableView.register(UINib(nibName: federationInvitationCellIdentifier, bundle: nil), forCellReuseIdentifier: federationInvitationCellIdentifier) self.tableView.backgroundView = backgroundView @@ -84,12 +84,12 @@ class FederationInvitationTableViewController: UITableViewController, Federation func showActivityIndicator() { self.modifyingViewIndicator.startAnimating() - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: modifyingViewIndicator) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: modifyingViewIndicator) } func hideActivityIndicator() { self.modifyingViewIndicator.stopAnimating() - self.navigationItem.leftBarButtonItem = nil + self.navigationItem.rightBarButtonItem = nil } // MARK: - Table view data source diff --git a/NextcloudTalk/GithubPermalinkViewController.swift b/NextcloudTalk/GithubPermalinkViewController.swift index 000cf0a8e..a986b93f7 100644 --- a/NextcloudTalk/GithubPermalinkViewController.swift +++ b/NextcloudTalk/GithubPermalinkViewController.swift @@ -7,15 +7,16 @@ import UIKit import Foundation import SwiftyAttributes -@objcMembers class GithubPermalinkViewController: UIViewController, UITextViewDelegate { +@objcMembers class GithubPermalinkViewController: UIViewController, UIScrollViewDelegate { @IBOutlet public weak var sourceWithNumbersTextView: UITextView! + @IBOutlet public weak var sourceWithNumbersScrollView: UIScrollView! @IBOutlet public weak var sourceWithoutNumbersTextView: UITextView! + @IBOutlet public weak var sourceWithoutNumbersScrollView: UIScrollView! @IBOutlet public weak var ownerLabel: UILabel! @IBOutlet public weak var repoLabel: UILabel! @IBOutlet public weak var fileLabel: UILabel! - @IBOutlet public weak var sourceCodeLeftConstraint: NSLayoutConstraint! - @IBOutlet public weak var lineNumbersRightConstraint: NSLayoutConstraint! + @IBOutlet public weak var scrollViewLeftConstraint: NSLayoutConstraint! private var url: String? private var sourceWithLineNumbers = NSAttributedString() @@ -82,8 +83,11 @@ import SwiftyAttributes self.sourceWithNumbersTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) self.sourceWithoutNumbersTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) - self.sourceWithNumbersTextView.layer.cornerRadius = 8 - self.sourceWithoutNumbersTextView.layer.cornerRadius = 8 + self.sourceWithNumbersScrollView.layer.cornerRadius = 8 + self.sourceWithoutNumbersScrollView.layer.cornerRadius = 8 + + self.sourceWithNumbersScrollView.layer.masksToBounds = true + self.sourceWithoutNumbersScrollView.layer.masksToBounds = true self.sourceWithNumbersTextView.textContainer.lineFragmentPadding = 0 self.sourceWithoutNumbersTextView.textContainer.lineFragmentPadding = 0 @@ -92,11 +96,11 @@ import SwiftyAttributes self.sourceWithoutNumbersTextView.attributedText = sourceWithoutLineNumbers // Set the delgate to synchronize scrolling - self.sourceWithoutNumbersTextView.delegate = self + self.sourceWithoutNumbersScrollView.delegate = self // We have to reduce the size of our overlaying view depending on how big the line numbers are // Take safe-area padding of 10 into account here - self.sourceCodeLeftConstraint.constant = self.lineNumberWidth + 10 + self.scrollViewLeftConstraint.constant = self.lineNumberWidth + 10 var formattedOwner = NSLocalizedString("Owner", comment: "Owner of a repository").attributedString + ": ".attributedString formattedOwner = formattedOwner.withFont(fontSemibold).withTextColor(.secondaryLabel) @@ -125,6 +129,6 @@ import SwiftyAttributes } func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.sourceWithNumbersTextView.contentOffset = self.sourceWithoutNumbersTextView.contentOffset + self.sourceWithNumbersTextView.contentOffset = CGPoint(x: 0, y: self.sourceWithoutNumbersScrollView.contentOffset.y) } } diff --git a/NextcloudTalk/GithubPermalinkViewController.xib b/NextcloudTalk/GithubPermalinkViewController.xib index 9b4f6e189..68a00f0d0 100644 --- a/NextcloudTalk/GithubPermalinkViewController.xib +++ b/NextcloudTalk/GithubPermalinkViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -12,11 +12,12 @@ - - + + + @@ -26,35 +27,65 @@ - + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, 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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + diff --git a/NextcloudTalk/RoomsTableViewController.m b/NextcloudTalk/RoomsTableViewController.m index 9e01d7084..3ff93e821 100644 --- a/NextcloudTalk/RoomsTableViewController.m +++ b/NextcloudTalk/RoomsTableViewController.m @@ -38,6 +38,7 @@ typedef enum RoomsSections { kRoomsSectionPendingFederationInvitation = 0, + kRoomsSectionArchivedConversations, kRoomsSectionRoomList } RoomsSections; @@ -46,6 +47,7 @@ @interface RoomsTableViewController () 0"]]; + return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"unreadMessages > 0 AND isArchived == %@", @(_showingArchivedRooms)]]; case kRoomsFilterMentioned: - return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"hasUnreadMention == YES"]]; + return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"hasUnreadMention == YES AND isArchived == %@", @(_showingArchivedRooms)]]; default: - return _allRooms; + return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isArchived == %@", @(_showingArchivedRooms)]]; } } @@ -912,7 +916,7 @@ - (void)updateMentionsIndicator for (int i = (int)lastVisibleRowIndexPath.row; i <= (int)_lastRoomWithMentionIndexPath.row && i < [_rooms count]; i++) { NCRoom *room = [_rooms objectAtIndex:i]; if (room.hasUnreadMention) { - _nextRoomWithMentionIndexPath = [NSIndexPath indexPathForRow:i inSection:1]; + _nextRoomWithMentionIndexPath = [NSIndexPath indexPathForRow:i inSection:kRoomsSectionRoomList]; break; } } @@ -938,7 +942,7 @@ - (void)calculateLastRoomWithMention for (int i = 0; i < _rooms.count; i++) { NCRoom *room = [_rooms objectAtIndex:i]; if (room.hasUnreadMention) { - _lastRoomWithMentionIndexPath = [NSIndexPath indexPathForRow:i inSection:1]; + _lastRoomWithMentionIndexPath = [NSIndexPath indexPathForRow:i inSection:kRoomsSectionRoomList]; } } } @@ -1061,6 +1065,32 @@ - (void)shareLinkFromRoom:(NCRoom *)room } } +- (void)archiveRoom:(NCRoom *)room +{ + TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; + + [[NCAPIController sharedInstance] archiveRoom:room.token forAccount:activeAccount completionBlock:^(BOOL success) { + if (!success) { + NSLog(@"Error archiving room"); + } + + [[NCRoomsManager sharedInstance] updateRoomsUpdatingUserStatus:YES onlyLastModified:NO]; + }]; +} + +- (void)unarchiveRoom:(NCRoom *)room +{ + TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; + + [[NCAPIController sharedInstance] unarchiveRoom:room.token forAccount:activeAccount completionBlock:^(BOOL success) { + if (!success) { + NSLog(@"Error unarchiving room"); + } + + [[NCRoomsManager sharedInstance] updateRoomsUpdatingUserStatus:YES onlyLastModified:NO]; + }]; +} + - (void)markRoomAsRead:(NCRoom *)room { [[NCAPIController sharedInstance] setChatReadMarker:room.lastMessage.messageId inRoom:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) { @@ -1083,7 +1113,7 @@ - (void)markRoomAsUnread:(NCRoom *)room - (void)addRoomToFavorites:(NCRoom *)room { - [[NCAPIController sharedInstance] addRoomToFavorites:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) { + [[NCAPIController sharedInstance] addRoomToFavorites:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] completionBlock:^(NSError *error) { if (error) { NSLog(@"Error adding room to favorites: %@", error.description); } @@ -1093,7 +1123,7 @@ - (void)addRoomToFavorites:(NCRoom *)room - (void)removeRoomFromFavorites:(NCRoom *)room { - [[NCAPIController sharedInstance] removeRoomFromFavorites:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) { + [[NCAPIController sharedInstance] removeRoomFromFavorites:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] completionBlock:^(NSError *error) { if (error) { NSLog(@"Error removing room from favorites: %@", error.description); } @@ -1155,7 +1185,7 @@ - (void)deleteRoom:(NCRoom *)room [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } - [[NCAPIController sharedInstance] deleteRoom:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSError *error) { + [[NCAPIController sharedInstance] deleteRoom:room.token forAccount:[[NCDatabaseManager sharedInstance] activeAccount] completionBlock:^(NSError *error) { if (error) { NSLog(@"Error deleting room: %@", error.description); } @@ -1196,12 +1226,22 @@ - (NSIndexPath *)indexPathForRoom:(NCRoom *)room }]; if (idx != NSNotFound) { - return [NSIndexPath indexPathForRow:idx inSection:1]; + return [NSIndexPath indexPathForRow:idx inSection:kRoomsSectionRoomList]; } return nil; } +- (NSArray *)archivedRooms +{ + return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isArchived == YES"]]; +} + +- (BOOL)areArchivedRoomsWithUnreadMentions +{ + return [_allRooms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"hasUnreadMention == YES AND isArchived == YES"]].count > 0; +} + - (void)showLeaveRoomLastModeratorErrorForRoom:(NCRoom *)room { UIAlertController *leaveRoomFailedDialog = @@ -1243,7 +1283,13 @@ - (void)presentSelectedMessageInChat:(NKSearchEntry *)message - (void)presentContextChatInRoom:(NCRoom *)room forMessageId:(NSInteger)messageId { - ContextChatViewController *contextChatViewController = [[ContextChatViewController alloc] initFor:room withMessage:@[] withHighlightId:0]; + TalkAccount *account = room.account; + + if (!account) { + return; + } + + ContextChatViewController *contextChatViewController = [[ContextChatViewController alloc] initForRoom:room withAccount:account withMessage:@[] withHighlightId:0]; contextChatViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(closeContextChat)]; NCChatController *chatController = [[NCChatController alloc] initForRoom:room]; @@ -1289,37 +1335,28 @@ - (void)createRoomForSelectedUser:(NCUser *)user - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return 2; + return 3; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == kRoomsSectionPendingFederationInvitation) { TalkAccount *account = [[NCDatabaseManager sharedInstance] activeAccount]; - - if (account.pendingFederationInvitations > 0) { - return 1; - } - - return 0; + return account.pendingFederationInvitations > 0 ? 1 : 0; } - return _rooms.count; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (tableView == self.tableView && indexPath.section == kRoomsSectionPendingFederationInvitation) { - return RoomInvitationViewCell.CellHeight; + if (section == kRoomsSectionArchivedConversations) { + return [self archivedRooms].count > 0 || _showingArchivedRooms ? 1 : 0; } - return RoomTableViewCell.cellHeight; + return _rooms.count; } - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { - if (tableView == self.tableView && indexPath.section == kRoomsSectionPendingFederationInvitation) { - // No swipe action for pending invitations + if (tableView == self.tableView && + (indexPath.section == kRoomsSectionPendingFederationInvitation || indexPath.section == kRoomsSectionArchivedConversations)) { + // No swipe action for pending invitations or archived conversations return nil; } @@ -1351,8 +1388,9 @@ - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwip - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { - if (tableView == self.tableView && indexPath.section == kRoomsSectionPendingFederationInvitation) { - // No swipe action for pending invitations + if (tableView == self.tableView && + (indexPath.section == kRoomsSectionPendingFederationInvitation || indexPath.section == kRoomsSectionArchivedConversations)) { + // No swipe action for pending invitations or archived conversations return nil; } @@ -1405,9 +1443,9 @@ - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipe - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == kRoomsSectionPendingFederationInvitation) { - RoomInvitationViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RoomInvitationViewCell.ReuseIdentifier]; + InfoLabelTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:InfoLabelTableViewCell.identifier]; if (!cell) { - cell = [[RoomInvitationViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:RoomInvitationViewCell.ReuseIdentifier]; + cell = [[InfoLabelTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:InfoLabelTableViewCell.identifier]; } // Pending federation invitations @@ -1427,7 +1465,43 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N NSRange range = NSMakeRange(0, [resultString length]); [resultString addAttribute:NSFontAttributeName value:[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline] range:range]; - cell.detailsLabel.attributedText = resultString; + cell.label.attributedText = resultString; + cell.separatorInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, CGFLOAT_MAX); + + return cell; + } + + if (indexPath.section == kRoomsSectionArchivedConversations) { + InfoLabelTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:InfoLabelTableViewCell.identifier]; + if (!cell) { + cell = [[InfoLabelTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:InfoLabelTableViewCell.identifier]; + } + + NSString *actionString = _showingArchivedRooms ? NSLocalizedString(@"Back to conversations", nil) : NSLocalizedString(@"Archived conversations", nil); + NSString *iconName = _showingArchivedRooms ? @"arrow.left" : @"archivebox"; + UIFont *resultFont = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; + + NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; + attachment.image = [[UIImage systemImageNamed:iconName] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + attachment.bounds = CGRectMake(0, roundf(resultFont.capHeight - 20) / 2, 24, 20); + + NSMutableAttributedString *resultString = [[NSMutableAttributedString alloc] initWithAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; + [resultString appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]]; + [resultString appendAttributedString:[[NSAttributedString alloc] initWithString:actionString]]; + + NSRange range = NSMakeRange(0, [resultString length]); + [resultString addAttribute:NSFontAttributeName value:[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline] range:range]; + + if (!_showingArchivedRooms && [self areArchivedRoomsWithUnreadMentions]) { + NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; + attachment.image = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:[NCAppBranding elementColor] renderingMode:UIImageRenderingModeAlwaysTemplate]; + attachment.bounds = CGRectMake(0, roundf(resultFont.capHeight - 20) / 2, 20, 20); + + [resultString appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]]; + [resultString appendAttributedString:[[NSAttributedString alloc] initWithAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]]; + } + + cell.label.attributedText = resultString; cell.separatorInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, CGFLOAT_MAX); return cell; @@ -1463,15 +1537,15 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [cell setUnreadWithMessages:room.unreadMessages mentioned:mentioned groupMentioned:NO]; } - [cell.roomImage setAvatarFor:room]; + [cell.avatarView setAvatarFor:room]; // Set favorite or call image if (room.hasCall) { - [cell.favoriteImage setTintColor:[UIColor systemRedColor]]; - [cell.favoriteImage setImage:[UIImage systemImageNamed:@"video.fill"]]; + [cell.avatarView.favoriteImageView setTintColor:[UIColor systemRedColor]]; + [cell.avatarView.favoriteImageView setImage:[UIImage systemImageNamed:@"video.fill"]]; } else if (room.isFavorite) { - [cell.favoriteImage setTintColor:[UIColor systemYellowColor]]; - [cell.favoriteImage setImage:[UIImage systemImageNamed:@"star.fill"]]; + [cell.avatarView.favoriteImageView setTintColor:[UIColor systemYellowColor]]; + [cell.avatarView.favoriteImageView setImage:[UIImage systemImageNamed:@"star.fill"]]; } cell.roomToken = room.token; @@ -1481,33 +1555,17 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)rcell forRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == kRoomsSectionPendingFederationInvitation || tableView != self.tableView) { + if (tableView != self.tableView || + indexPath.section == kRoomsSectionPendingFederationInvitation || + indexPath.section == kRoomsSectionArchivedConversations) { return; } RoomTableViewCell *cell = (RoomTableViewCell *)rcell; NCRoom *room = [_rooms objectAtIndex:indexPath.row]; - //Show User Status - if (room.type == kNCRoomTypeOneToOne && [room.status length] != 0) { - if (![room.status isEqualToString:@"dnd"] && [room.statusIcon length] != 0) { - [cell setUserStatusIcon:room.statusIcon]; - } else { - [cell setUserStatus:room.status]; - } - } else if (room.isPublic) { - UIImageSymbolConfiguration *conf = [UIImageSymbolConfiguration configurationWithPointSize:12]; - UIImage *publicRoomImage = [UIImage systemImageNamed:@"link"]; - publicRoomImage = [publicRoomImage imageWithTintColor:[UIColor labelColor] renderingMode:UIImageRenderingModeAlwaysOriginal]; - publicRoomImage = [publicRoomImage imageByApplyingSymbolConfiguration:conf]; - [cell setUserStatusIconWithImage:publicRoomImage]; - } else if (room.isFederated) { - UIImageSymbolConfiguration *conf = [UIImageSymbolConfiguration configurationWithPointSize:14]; - UIImage *publicRoomImage = [UIImage systemImageNamed:@"globe"]; - publicRoomImage = [publicRoomImage imageWithTintColor:[UIColor labelColor] renderingMode:UIImageRenderingModeAlwaysOriginal]; - publicRoomImage = [publicRoomImage imageByApplyingSymbolConfiguration:conf]; - [cell setUserStatusIconWithImage:publicRoomImage]; - } + UIColor *backgroundColor = cell.backgroundConfiguration.backgroundColor ? cell.backgroundConfiguration.backgroundColor : cell.backgroundColor; + [cell.avatarView setStatusFor:room with:backgroundColor]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath @@ -1531,6 +1589,14 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath return; } + if (tableView == self.tableView && indexPath.section == kRoomsSectionArchivedConversations) { + _showingArchivedRooms = !_showingArchivedRooms; + [UIView transitionWithView:self.tableView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + [self filterRooms]; + } completion:nil]; + return; + } + if (tableView == _resultTableViewController.tableView) { // Messages NKSearchEntry *message = [_resultTableViewController messageForIndexPath:indexPath]; @@ -1553,7 +1619,9 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point { - if (![tableView isEqual:self.tableView]) { + if (tableView != self.tableView || + indexPath.section == kRoomsSectionPendingFederationInvitation || + indexPath.section == kRoomsSectionArchivedConversations) { return nil; } @@ -1630,6 +1698,23 @@ - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuCo [actions addObject:notificationActions]; } + // Archive conversation + if ([[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityArchivedConversationsV2]) { + if (room.isArchived) { + UIAction *unarchiveAction = [UIAction actionWithTitle:NSLocalizedString(@"Unarchive conversation", nil) image:[UIImage systemImageNamed:@"arrow.up.bin"] identifier:nil handler:^(UIAction *action) { + [weakSelf unarchiveRoom:room]; + }]; + + [actions addObject:unarchiveAction]; + } else { + UIAction *archiveAction = [UIAction actionWithTitle:NSLocalizedString(@"Archive conversation", nil) image:[UIImage systemImageNamed:@"archivebox"] identifier:nil handler:^(UIAction *action) { + [weakSelf archiveRoom:room]; + }]; + + [actions addObject:archiveAction]; + } + } + // Room info UIAction *roomInfoAction = [UIAction actionWithTitle:NSLocalizedString(@"Conversation settings", nil) image:[UIImage systemImageNamed:@"gearshape"] identifier:nil handler:^(UIAction *action) { [weakSelf presentRoomInfoForRoom:room]; @@ -1731,7 +1816,7 @@ - (void)highlightSelectedRoom }]; if (idx != NSNotFound) { - NSIndexPath* indexPath = [NSIndexPath indexPathForRow:idx inSection:1]; + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:idx inSection:kRoomsSectionRoomList]; [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; } } else { diff --git a/NextcloudTalk/RoundedNumberView.h b/NextcloudTalk/RoundedNumberView.h deleted file mode 100644 index 19e5339cf..000000000 --- a/NextcloudTalk/RoundedNumberView.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import - -typedef NS_ENUM(NSInteger, HighlightType) { - kHighlightTypeNone = 0, - kHighlightTypeBorder, - kHighlightTypeImportant -}; - -@interface RoundedNumberView : UIView - -@property (nonatomic, assign) NSInteger number; -@property (nonatomic, strong) UIColor *numberColor; -@property (nonatomic, assign) HighlightType highlightType; - -@end diff --git a/NextcloudTalk/RoundedNumberView.m b/NextcloudTalk/RoundedNumberView.m deleted file mode 100644 index bb0adbfe3..000000000 --- a/NextcloudTalk/RoundedNumberView.m +++ /dev/null @@ -1,128 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import "RoundedNumberView.h" - -#import "NCAppBranding.h" - -#define kRoundedNumberViewImportantBackgroundColor [UIColor colorWithRed:0.00 green:0.51 blue:0.79 alpha:1.0] //#0082C9 -#define kRoundedNumberViewImportantTextColor [UIColor whiteColor] -#define kRoundedNumberViewDefaultBackgroundColor [UIColor colorWithRed:0.84 green:0.84 blue:0.84 alpha:1.0] //#d5d5d5 -#define kRoundedNumberViewDefaultTextColor [UIColor blackColor] -#define kRoundedNumberViewCounterLimit 9999 - -@interface RoundedNumberView () -@property (nonatomic, strong) UILabel *numberLabel; -@end - -@implementation RoundedNumberView - -- (id)initWithFrame:(CGRect)frame -{ - return [self initWithNumber:0]; -} - - -- (id)init -{ - return [self initWithNumber:0]; -} - - -- (id)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - _number = 0; - [self addNecessaryViews]; - [self setup]; - } - return self; -} - - -- (id)initWithNumber:(NSInteger)number -{ - self = [super initWithFrame:CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)]; - if (self) { - _number = 0; - [self addNecessaryViews]; - [self setup]; - } - return self; -} - - -// This method should be called only once -- (void)addNecessaryViews -{ - self.backgroundColor = kRoundedNumberViewDefaultBackgroundColor; - self.numberLabel = [[UILabel alloc] init]; - self.numberLabel.font = [UIFont boldSystemFontOfSize:14]; - self.numberLabel.backgroundColor = [UIColor clearColor]; - _numberColor = kRoundedNumberViewDefaultTextColor; - [self addSubview:self.numberLabel]; -} - - -- (void)setup -{ - NSInteger counter = _number; - self.numberLabel.textColor = _numberColor; - self.numberLabel.text = [NSString stringWithFormat:@"%ld", (long)counter]; - if (counter > kRoundedNumberViewCounterLimit) { - self.numberLabel.text = [NSString stringWithFormat:@"%d+", kRoundedNumberViewCounterLimit]; - } - [self.numberLabel sizeToFit]; - CGFloat frameWidth = self.numberLabel.frame.size.width + 16; - CGFloat frameHeight = self.numberLabel.frame.size.height + self.numberLabel.frame.size.height / 2; - self.frame = CGRectMake(0, 0, (frameWidth >= frameHeight) ? frameWidth : frameHeight, frameHeight); - self.layer.cornerRadius = self.frame.size.height / 2; - [self.numberLabel setCenter:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)]; -} - - -- (void)setNumber:(NSInteger)number -{ - if (_number != number) { - _number = number; - [self setup]; - } -} - - -- (void)setNumberColor:(UIColor *)numberColor -{ - if (_numberColor != numberColor) { - _numberColor = numberColor; - self.numberLabel.textColor = _numberColor; - } -} - -- (void)setHighlightType:(HighlightType)highlightType -{ - _highlightType = highlightType; - - self.layer.borderWidth = 0; - - switch (highlightType) { - case kHighlightTypeNone: - self.backgroundColor = [NCAppBranding placeholderColor]; - _numberColor = nil; - break; - case kHighlightTypeBorder: - self.backgroundColor = [UIColor systemBackgroundColor]; - _numberColor = [NCAppBranding elementColor]; - self.layer.borderWidth = 2; - self.layer.borderColor = [NCAppBranding elementColor].CGColor; - break; - case kHighlightTypeImportant: - self.backgroundColor = [NCAppBranding themeColor]; - _numberColor = [NCAppBranding themeTextColor]; - break; - } -} - -@end diff --git a/NextcloudTalk/SearchTableViewController.m b/NextcloudTalk/SearchTableViewController.m index c820f254c..9cd2803f3 100644 --- a/NextcloudTalk/SearchTableViewController.m +++ b/NextcloudTalk/SearchTableViewController.m @@ -102,8 +102,12 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.labelTitle.text = contact.name; - [cell.contactImage setActorAvatarForId:contact.userId withType:contact.source withDisplayName:contact.name withRoomToken:nil]; - + TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; + + if (activeAccount) { + [cell.contactImage setActorAvatarForId:contact.userId withType:contact.source withDisplayName:contact.name withRoomToken:nil using:activeAccount]; + } + return cell; } diff --git a/NextcloudTalk/SettingsTableViewController.swift b/NextcloudTalk/SettingsTableViewController.swift index 4868b51fb..e427305db 100644 --- a/NextcloudTalk/SettingsTableViewController.swift +++ b/NextcloudTalk/SettingsTableViewController.swift @@ -389,7 +389,7 @@ class SettingsTableViewController: UITableViewController, UITextFieldDelegate, U } failedPhoneNumberDialog.addAction(retryAction) - let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .default, handler: nil) + let cancelAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil) failedPhoneNumberDialog.addAction(cancelAction) self.present(failedPhoneNumberDialog, animated: true, completion: nil) @@ -689,6 +689,7 @@ class SettingsTableViewController: UITableViewController, UITextFieldDelegate, U cell.textLabel?.text = activeAccount.userDisplayName cell.textLabel?.font = .preferredFont(for: .title2, weight: .medium) cell.detailTextLabel?.text = activeAccount.server.replacingOccurrences(of: "https://", with: "") + cell.detailTextLabel?.lineBreakMode = .byCharWrapping cell.imageView?.image = self.getProfilePicture(for: activeAccount)?.cropToCircle(withSize: CGSize(width: 60, height: 60)) cell.accessoryType = .disclosureIndicator return cell @@ -880,15 +881,17 @@ extension SettingsTableViewController { let cell: SettingsTableViewCell = tableView.dequeueOrCreateCell(withIdentifier: "AccountCellIdentifier", style: .subtitle) cell.textLabel?.text = account.userDisplayName cell.detailTextLabel?.text = account.server.replacingOccurrences(of: "https://", with: "") + cell.detailTextLabel?.lineBreakMode = .byCharWrapping if let accountImage = self.getProfilePicture(for: account) { cell.setSettingsImage(image: NCUtils.roundedImage(fromImage: accountImage), renderingMode: .alwaysOriginal) } if account.unreadBadgeNumber > 0 { - let badgeView = RoundedNumberView() - badgeView.highlightType = .important - badgeView.number = account.unreadBadgeNumber + let badgeView = BadgeView(frame: .zero) + badgeView.badgeColor = NCAppBranding.themeColor() + badgeView.badgeTextColor = NCAppBranding.themeTextColor() + badgeView.setBadgeNumber(account.unreadBadgeNumber) cell.accessoryView = badgeView } diff --git a/NextcloudTalk/ShareLocationViewController.m b/NextcloudTalk/ShareLocationViewController.m index a4d3b9521..4e5ddcd09 100644 --- a/NextcloudTalk/ShareLocationViewController.m +++ b/NextcloudTalk/ShareLocationViewController.m @@ -150,7 +150,7 @@ - (void)showAuthorizationStatusDeniedAlert [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; }]; [alert addAction:settingsButton]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]]; + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } diff --git a/NextcloudTalk/SystemMessageTableViewCell.h b/NextcloudTalk/SystemMessageTableViewCell.h index 1f6e29092..0a222c714 100644 --- a/NextcloudTalk/SystemMessageTableViewCell.h +++ b/NextcloudTalk/SystemMessageTableViewCell.h @@ -12,7 +12,6 @@ static CGFloat kSystemMessageCellMinimumHeight = 30.0; static NSString *SystemMessageCellIdentifier = @"SystemMessageCellIdentifier"; -static NSString *InvisibleSystemMessageCellIdentifier = @"InvisibleSystemMessageCellIdentifier"; @protocol SystemMessageTableViewCellDelegate diff --git a/NextcloudTalk/SystemMessageTableViewCell.m b/NextcloudTalk/SystemMessageTableViewCell.m index 6da2fe5b2..45fd22b20 100644 --- a/NextcloudTalk/SystemMessageTableViewCell.m +++ b/NextcloudTalk/SystemMessageTableViewCell.m @@ -27,10 +27,6 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr - (void)configureSubviews { - if ([self.reuseIdentifier isEqualToString:InvisibleSystemMessageCellIdentifier]) { - return; - } - [self.contentView addSubview:self.dateLabel]; [self.contentView addSubview:self.bodyTextView]; [self.contentView addSubview:self.collapseButton]; @@ -58,7 +54,7 @@ - (void)prepareForReuse [super prepareForReuse]; if (!self.didCreateSubviews) { - [self configureSubviews]; + return; } self.selectionStyle = UITableViewCellSelectionStyleNone; diff --git a/NextcloudTalk/TalkActor.swift b/NextcloudTalk/TalkActor.swift index 7b6069fde..72c17842c 100644 --- a/NextcloudTalk/TalkActor.swift +++ b/NextcloudTalk/TalkActor.swift @@ -31,11 +31,16 @@ import SwiftyAttributes /// Takes deleted users and guests into account and returns it as `secondaryLabel` /// This also appends a potential `cloudId` as `tertiaryLabel` in parentheses public var attributedDisplayName: NSMutableAttributedString { + let displayName = self.displayName let titleLabel = displayName.withTextColor(.secondaryLabel) if let remoteServer = cloudId { let remoteServerString = " (\(String(remoteServer)))" titleLabel.append(remoteServerString.withTextColor(.tertiaryLabel)) + } else if isGuest, !rawDisplayName.isEmpty { + // Show guest indication only when we did not use the default "Guest" name + let guestString = " (\(NSLocalizedString("guest", comment: "")))" + titleLabel.append(guestString.withTextColor(.tertiaryLabel)) } return titleLabel @@ -55,6 +60,10 @@ import SwiftyAttributes return type == "federated_users" } + public var isGuest: Bool { + return type == "guests" || type == "emails" + } + public var cloudId: String? { guard isFederated, let remoteServer = id?.split(separator: "@").last else { return nil } diff --git a/NextcloudTalk/TalkCapabilities.h b/NextcloudTalk/TalkCapabilities.h index 792bdabf3..7a9ffe4c1 100644 --- a/NextcloudTalk/TalkCapabilities.h +++ b/NextcloudTalk/TalkCapabilities.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @property BOOL federationOutgoingEnabled; @property BOOL federationOnlyTrustedServers; @property NSInteger maxGifSize; +@property NSInteger summaryThreshold; @end diff --git a/NextcloudTalk/UserAbsence.swift b/NextcloudTalk/UserAbsence.swift new file mode 100644 index 000000000..79c545abe --- /dev/null +++ b/NextcloudTalk/UserAbsence.swift @@ -0,0 +1,32 @@ +// +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +@objcMembers public class UserAbsence: NSObject { + + // See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-out-of-office-api.html + public var id: String? + public var userId: String? + public var startDate: Int? + public var endDate: Int? + public var shortMessage: String? + public var message: String? + public var replacementUserId: String? + public var replacementUserDisplayName: String? + + init(dictionary: [String: Any]) { + super.init() + + self.id = dictionary["id"] as? String + self.userId = dictionary["userId"] as? String + self.startDate = dictionary["startDate"] as? Int + self.endDate = dictionary["endDate"] as? Int + self.shortMessage = dictionary["status"] as? String + self.message = dictionary["message"] as? String + self.replacementUserId = dictionary["replacementUserId"] as? String + self.replacementUserDisplayName = dictionary["replacementUserDisplayName"] as? String + } +} diff --git a/NextcloudTalk/UserStatusOptionsSwiftUI.swift b/NextcloudTalk/UserStatusOptionsSwiftUI.swift index d7ab7383f..070b6e7e9 100644 --- a/NextcloudTalk/UserStatusOptionsSwiftUI.swift +++ b/NextcloudTalk/UserStatusOptionsSwiftUI.swift @@ -51,8 +51,8 @@ struct UserStatusOptionsSwiftUI: View { } func setActiveUserStatus(userStatus: String) { - let activeAcoount: TalkAccount = NCDatabaseManager.sharedInstance().activeAccount() - NCAPIController.sharedInstance().setUserStatus(userStatus, for: activeAcoount) { _ in + let activeAccount: TalkAccount = NCDatabaseManager.sharedInstance().activeAccount() + NCAPIController.sharedInstance().setUserStatus(userStatus, for: activeAccount) { _ in getActiveUserStatus() dismiss() changed.toggle() diff --git a/NextcloudTalk/UserStatusSwiftUIView.swift b/NextcloudTalk/UserStatusSwiftUIView.swift index c3b328110..6d46d9392 100644 --- a/NextcloudTalk/UserStatusSwiftUIView.swift +++ b/NextcloudTalk/UserStatusSwiftUIView.swift @@ -54,7 +54,7 @@ struct UserStatusSwiftUIView: View { Button(action: { dismiss() }) { - Text("Cancel") + Text("Close") .foregroundColor(Color(NCAppBranding.themeTextColor())) } } diff --git a/NextcloudTalk/VoiceMessageTranscribeViewController.m b/NextcloudTalk/VoiceMessageTranscribeViewController.m index 992b855a3..24c5979d4 100644 --- a/NextcloudTalk/VoiceMessageTranscribeViewController.m +++ b/NextcloudTalk/VoiceMessageTranscribeViewController.m @@ -172,7 +172,7 @@ - (void)showSpeechRecognitionNotAvailable [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; }]; [alert addAction:settingsButton]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [self closeViewController]; diff --git a/NextcloudTalk/ar.lproj/Localizable.strings b/NextcloudTalk/ar.lproj/Localizable.strings index 618d5e280..528022b49 100644 Binary files a/NextcloudTalk/ar.lproj/Localizable.strings and b/NextcloudTalk/ar.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ast.lproj/Localizable.strings b/NextcloudTalk/ast.lproj/Localizable.strings index 037d448ad..fe87316e0 100644 Binary files a/NextcloudTalk/ast.lproj/Localizable.strings and b/NextcloudTalk/ast.lproj/Localizable.strings differ diff --git a/NextcloudTalk/bg_BG.lproj/Localizable.strings b/NextcloudTalk/bg_BG.lproj/Localizable.strings index 1ddba2d50..dd4365c26 100644 Binary files a/NextcloudTalk/bg_BG.lproj/Localizable.strings and b/NextcloudTalk/bg_BG.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ca.lproj/Localizable.strings b/NextcloudTalk/ca.lproj/Localizable.strings index 63216e3ca..e8722a049 100644 Binary files a/NextcloudTalk/ca.lproj/Localizable.strings and b/NextcloudTalk/ca.lproj/Localizable.strings differ diff --git a/NextcloudTalk/cs.lproj/Localizable.strings b/NextcloudTalk/cs.lproj/Localizable.strings index 7dd71993f..b623698fa 100644 Binary files a/NextcloudTalk/cs.lproj/Localizable.strings and b/NextcloudTalk/cs.lproj/Localizable.strings differ diff --git a/NextcloudTalk/da.lproj/Localizable.strings b/NextcloudTalk/da.lproj/Localizable.strings index 376b6495a..bf084ea55 100644 Binary files a/NextcloudTalk/da.lproj/Localizable.strings and b/NextcloudTalk/da.lproj/Localizable.strings differ diff --git a/NextcloudTalk/de.lproj/Localizable.strings b/NextcloudTalk/de.lproj/Localizable.strings index 0d3dad430..667eee403 100644 Binary files a/NextcloudTalk/de.lproj/Localizable.strings and b/NextcloudTalk/de.lproj/Localizable.strings differ diff --git a/NextcloudTalk/el.lproj/Localizable.strings b/NextcloudTalk/el.lproj/Localizable.strings index 445bf3771..e5f042a4c 100644 Binary files a/NextcloudTalk/el.lproj/Localizable.strings and b/NextcloudTalk/el.lproj/Localizable.strings differ diff --git a/NextcloudTalk/en-GB.lproj/Localizable.strings b/NextcloudTalk/en-GB.lproj/Localizable.strings index 3fdd17019..2959aaf3c 100644 Binary files a/NextcloudTalk/en-GB.lproj/Localizable.strings and b/NextcloudTalk/en-GB.lproj/Localizable.strings differ diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index bb5789d5d..abdc1c111 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -1,6 +1,12 @@ /* No comment provided by engineer. */ "%@ invitation" = "%@ invitation"; +/* '%@' is the name of a user */ +"%@ is out of office" = "%@ is out of office"; + +/* '%@' is the name of a user */ +"%@ is out of office today" = "%@ is out of office today"; + /* {app name} is not installed */ "%@ not installed" = "%@ not installed"; @@ -125,7 +131,7 @@ "All notifications are muted" = "All notifications are muted"; /* No comment provided by engineer. */ -"Allow guests" = "Allow guests"; +"Allow guests to join this conversation via link" = "Allow guests to join this conversation via link"; /* '@all' should not be translated */ "Allow participants to mention @all" = "Allow participants to mention @all"; @@ -277,6 +283,9 @@ /* No comment provided by engineer. */ "An error occurred while sharing the file" = "An error occurred while sharing the file"; +/* No comment provided by engineer. */ +"An error occurred while testing push notifications" = "An error occurred while testing push notifications"; + /* No comment provided by engineer. */ "an hour" = "an hour"; @@ -304,6 +313,15 @@ /* No comment provided by engineer. */ "Appear offline" = "Appear offline"; +/* No comment provided by engineer. */ +"Archive conversation" = "Archive conversation"; + +/* No comment provided by engineer. */ +"Archived conversations" = "Archived conversations"; + +/* No comment provided by engineer. */ +"Archived conversations are hidden from the conversation list by default. They will only be shown when you open archived conversations list." = "Archived conversations are hidden from the conversation list by default. They will only be shown when you open archived conversations list."; + /* Alice and Bob are typing… */ "are typing…" = "are typing…"; @@ -322,6 +340,9 @@ /* No comment provided by engineer. */ "Away" = "Away"; +/* No comment provided by engineer. */ +"Back to conversations" = "Back to conversations"; + /* Ban a user/guest */ "Ban" = "Ban"; @@ -337,6 +358,9 @@ /* No comment provided by engineer. */ "Banned users and guests" = "Banned users and guests"; +/* Bold text */ +"Bold" = "Bold"; + /* No comment provided by engineer. */ "bot" = "bot"; @@ -439,6 +463,9 @@ /* No comment provided by engineer. */ "Close" = "Close"; +/* Code block */ +"Code" = "Code"; + /* No comment provided by engineer. */ "Configuration" = "Configuration"; @@ -505,6 +532,9 @@ /* No comment provided by engineer. */ "Could not add participant" = "Could not add participant"; +/* No comment provided by engineer. */ +"Could not archive conversation" = "Could not archive conversation"; + /* No comment provided by engineer. */ "Could not ban participant" = "Could not ban participant"; @@ -595,6 +625,9 @@ /* No comment provided by engineer. */ "Could not share file" = "Could not share file"; +/* No comment provided by engineer. */ +"Could not unarchive conversation" = "Could not unarchive conversation"; + /* No comment provided by engineer. */ "Create" = "Create"; @@ -797,10 +830,10 @@ "edited" = "edited"; /* A message was edited by ... */ -"Edited by" = "Edited by"; +"edited by" = "edited by"; /* A message was edited by ... */ -"edited by" = "edited by"; +"Edited by" = "Edited by"; /* No comment provided by engineer. */ "Editing Message" = "Editing Message"; @@ -811,6 +844,9 @@ /* No comment provided by engineer. */ "Email" = "Email"; +/* No comment provided by engineer. */ +"Empty summary response" = "Empty summary response"; + /* No comment provided by engineer. */ "Enable" = "Enable"; @@ -901,6 +937,15 @@ /* No comment provided by engineer. */ "Full name" = "Full name"; +/* No comment provided by engineer. */ +"Generate summary" = "Generate summary"; + +/* No comment provided by engineer. */ +"Generating summary of unread messages" = "Generating summary of unread messages"; + +/* No comment provided by engineer. */ +"Generating summary of unread messages failed" = "Generating summary of unread messages failed"; + /* No comment provided by engineer. */ "Get source code" = "Get source code"; @@ -925,6 +970,9 @@ /* No comment provided by engineer. */ "Hang up" = "Hang up"; +/* No comment provided by engineer. */ +"Hide" = "Hide"; + /* No comment provided by engineer. */ "hours" = "hours"; @@ -979,6 +1027,9 @@ /* No comment provided by engineer. */ "It seems that there is no internet connection." = "It seems that there is no internet connection."; +/* Italic text */ +"Italic" = "Italic"; + /* No comment provided by engineer. */ "Join a conversation or start a new one" = "Join a conversation or start a new one"; @@ -1187,8 +1238,13 @@ "No actions available" = "No actions available"; /* No comment provided by engineer. */ + "No active account found" = "No active account found"; +/* No comment provided by engineer. */ + +"No AI provider available or summarizing failed" = "No AI provider available or summarizing failed"; + /* No comment provided by engineer. */ "No banned users or guests" = "No banned users or guests"; @@ -1198,6 +1254,9 @@ /* No comment provided by engineer. */ "No files in here" = "No files in here"; +/* No comment provided by engineer. */ +"No messages found to summarize" = "No messages found to summarize"; + /* No comment provided by engineer. */ "No messages yet, start the conversation!" = "No messages yet, start the conversation!"; @@ -1255,6 +1314,9 @@ /* No comment provided by engineer. */ "Once a conversation is left, to rejoin a closed conversation, an invite is needed. An open conversation can be rejoined at any time." = "Once a conversation is left, to rejoin a closed conversation, an invite is needed. An open conversation can be rejoined at any time."; +/* No comment provided by engineer. */ +"Once a conversation is unarchived, it will be shown by default again." = "Once a conversation is unarchived, it will be shown by default again."; + /* No comment provided by engineer. */ "Online" = "Online"; @@ -1450,6 +1512,9 @@ /* No comment provided by engineer. */ "Remove team and members" = "Remove team and members"; +/* Replacement in case of out of office */ +"Replacement" = "Replacement"; + /* No comment provided by engineer. */ "Reply" = "Reply"; @@ -1477,6 +1542,9 @@ /* Save conversation description */ "Save" = "Save"; +/* No comment provided by engineer. */ +"Save to 'Note to self'" = "Save to 'Note to self'"; + /* No comment provided by engineer. */ "Say hi to your friends and colleagues!" = "Say hi to your friends and colleagues!"; @@ -1639,6 +1707,9 @@ /* No comment provided by engineer. */ "Stop screensharing" = "Stop screensharing"; +/* Strikethrough text */ +"Strikethrough" = "Strikethrough"; + /* No comment provided by engineer. */ "STUN servers" = "STUN servers"; @@ -1657,12 +1728,27 @@ /* No comment provided by engineer. */ "Talk" = "Talk"; +/* Talk to a user */ +"Talk to" = "Talk to"; + /* No comment provided by engineer. */ "Tap and hold to record a voice message" = "Tap and hold to record a voice message"; /* No comment provided by engineer. */ "Tap and hold to record a voice message, release the button to send it." = "Tap and hold to record a voice message, release the button to send it."; +/* No comment provided by engineer. */ +"Test failed" = "Test failed"; + +/* No comment provided by engineer. */ +"Test push notifications" = "Test push notifications"; + +/* No comment provided by engineer. */ +"Test results" = "Test results"; + +/* No comment provided by engineer. */ +"Test results copied" = "Test results copied"; + /* No comment provided by engineer. */ "The app is too old and no longer supported by this server." = "The app is too old and no longer supported by this server."; @@ -1690,6 +1776,12 @@ /* The meeting start time will be displayed after this text e.g (This meeting is scheduled for tomorrow at 10:00) */ "This meeting is scheduled for" = "This meeting is scheduled for"; +/* No comment provided by engineer. */ +"This might take a moment" = "This might take a moment"; + +/* No comment provided by engineer. */ +"This summary is AI generated and may contain mistakes." = "This summary is AI generated and may contain mistakes."; + /* No comment provided by engineer. */ "This usually indicates that this device was previously used for an account, which was not properly removed from the server." = "This usually indicates that this device was previously used for an account, which was not properly removed from the server."; @@ -1759,6 +1851,9 @@ /* No comment provided by engineer. */ "Unable to open file" = "Unable to open file"; +/* No comment provided by engineer. */ +"Unarchive conversation" = "Unarchive conversation"; + /* No comment provided by engineer. */ "Unavailable" = "Unavailable"; diff --git a/NextcloudTalk/es-EC.lproj/Localizable.strings b/NextcloudTalk/es-EC.lproj/Localizable.strings index 63d45ef1d..05e909d1e 100644 Binary files a/NextcloudTalk/es-EC.lproj/Localizable.strings and b/NextcloudTalk/es-EC.lproj/Localizable.strings differ diff --git a/NextcloudTalk/es.lproj/Localizable.strings b/NextcloudTalk/es.lproj/Localizable.strings index cb35a4ec3..b227b9ac9 100644 Binary files a/NextcloudTalk/es.lproj/Localizable.strings and b/NextcloudTalk/es.lproj/Localizable.strings differ diff --git a/NextcloudTalk/eu.lproj/Localizable.strings b/NextcloudTalk/eu.lproj/Localizable.strings index 000cbe18a..9feac9bd0 100644 Binary files a/NextcloudTalk/eu.lproj/Localizable.strings and b/NextcloudTalk/eu.lproj/Localizable.strings differ diff --git a/NextcloudTalk/fa.lproj/Localizable.strings b/NextcloudTalk/fa.lproj/Localizable.strings index 92c7e2aef..1d938a1d7 100644 Binary files a/NextcloudTalk/fa.lproj/Localizable.strings and b/NextcloudTalk/fa.lproj/Localizable.strings differ diff --git a/NextcloudTalk/fi-FI.lproj/Localizable.strings b/NextcloudTalk/fi-FI.lproj/Localizable.strings index d102e73df..def86b965 100644 Binary files a/NextcloudTalk/fi-FI.lproj/Localizable.strings and b/NextcloudTalk/fi-FI.lproj/Localizable.strings differ diff --git a/NextcloudTalk/fr.lproj/Localizable.strings b/NextcloudTalk/fr.lproj/Localizable.strings index 677cd7c1c..baf98cfb0 100644 Binary files a/NextcloudTalk/fr.lproj/Localizable.strings and b/NextcloudTalk/fr.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ga.lproj/Localizable.strings b/NextcloudTalk/ga.lproj/Localizable.strings index 5a247a6ec..fdc8bf89d 100644 Binary files a/NextcloudTalk/ga.lproj/Localizable.strings and b/NextcloudTalk/ga.lproj/Localizable.strings differ diff --git a/NextcloudTalk/gl.lproj/InfoPlist.strings b/NextcloudTalk/gl.lproj/InfoPlist.strings index b19b5ef8a..f3d8ecf3e 100644 Binary files a/NextcloudTalk/gl.lproj/InfoPlist.strings and b/NextcloudTalk/gl.lproj/InfoPlist.strings differ diff --git a/NextcloudTalk/gl.lproj/Localizable.strings b/NextcloudTalk/gl.lproj/Localizable.strings index 97bce4b6a..b5d19c2ca 100644 Binary files a/NextcloudTalk/gl.lproj/Localizable.strings and b/NextcloudTalk/gl.lproj/Localizable.strings differ diff --git a/NextcloudTalk/he.lproj/Localizable.strings b/NextcloudTalk/he.lproj/Localizable.strings index e4bd48699..c3b87c2c3 100644 Binary files a/NextcloudTalk/he.lproj/Localizable.strings and b/NextcloudTalk/he.lproj/Localizable.strings differ diff --git a/NextcloudTalk/hr.lproj/Localizable.strings b/NextcloudTalk/hr.lproj/Localizable.strings index dd0f1fb30..881c7350f 100644 Binary files a/NextcloudTalk/hr.lproj/Localizable.strings and b/NextcloudTalk/hr.lproj/Localizable.strings differ diff --git a/NextcloudTalk/hu.lproj/Localizable.strings b/NextcloudTalk/hu.lproj/Localizable.strings index f6a8b29d9..2c5fc0eb5 100644 Binary files a/NextcloudTalk/hu.lproj/Localizable.strings and b/NextcloudTalk/hu.lproj/Localizable.strings differ diff --git a/NextcloudTalk/is.lproj/Localizable.strings b/NextcloudTalk/is.lproj/Localizable.strings index e5bb97065..2caf18256 100644 Binary files a/NextcloudTalk/is.lproj/Localizable.strings and b/NextcloudTalk/is.lproj/Localizable.strings differ diff --git a/NextcloudTalk/it.lproj/Localizable.strings b/NextcloudTalk/it.lproj/Localizable.strings index 99731606e..73c724ad6 100644 Binary files a/NextcloudTalk/it.lproj/Localizable.strings and b/NextcloudTalk/it.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ja.lproj/Localizable.strings b/NextcloudTalk/ja.lproj/Localizable.strings index 87b9f5fb8..3f529d153 100644 Binary files a/NextcloudTalk/ja.lproj/Localizable.strings and b/NextcloudTalk/ja.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ko.lproj/Localizable.strings b/NextcloudTalk/ko.lproj/Localizable.strings index 0250f5065..7cd61baa8 100644 Binary files a/NextcloudTalk/ko.lproj/Localizable.strings and b/NextcloudTalk/ko.lproj/Localizable.strings differ diff --git a/NextcloudTalk/lt_LT.lproj/Localizable.strings b/NextcloudTalk/lt_LT.lproj/Localizable.strings index be21bce9b..5d5a5d825 100644 Binary files a/NextcloudTalk/lt_LT.lproj/Localizable.strings and b/NextcloudTalk/lt_LT.lproj/Localizable.strings differ diff --git a/NextcloudTalk/mk.lproj/Localizable.strings b/NextcloudTalk/mk.lproj/Localizable.strings index 18ce2eaed..579388db7 100644 Binary files a/NextcloudTalk/mk.lproj/Localizable.strings and b/NextcloudTalk/mk.lproj/Localizable.strings differ diff --git a/NextcloudTalk/nb-NO.lproj/Localizable.strings b/NextcloudTalk/nb-NO.lproj/Localizable.strings index f8699d333..bb677357d 100644 Binary files a/NextcloudTalk/nb-NO.lproj/Localizable.strings and b/NextcloudTalk/nb-NO.lproj/Localizable.strings differ diff --git a/NextcloudTalk/nl.lproj/Localizable.strings b/NextcloudTalk/nl.lproj/Localizable.strings index 38568a32d..8ff4d632e 100644 Binary files a/NextcloudTalk/nl.lproj/Localizable.strings and b/NextcloudTalk/nl.lproj/Localizable.strings differ diff --git a/NextcloudTalk/oc.lproj/Localizable.strings b/NextcloudTalk/oc.lproj/Localizable.strings index 2e6eb79ce..b65c09cde 100644 Binary files a/NextcloudTalk/oc.lproj/Localizable.strings and b/NextcloudTalk/oc.lproj/Localizable.strings differ diff --git a/NextcloudTalk/pl.lproj/Localizable.strings b/NextcloudTalk/pl.lproj/Localizable.strings index 12b990a36..9a4e9da6e 100644 Binary files a/NextcloudTalk/pl.lproj/Localizable.strings and b/NextcloudTalk/pl.lproj/Localizable.strings differ diff --git a/NextcloudTalk/pt-BR.lproj/Localizable.strings b/NextcloudTalk/pt-BR.lproj/Localizable.strings index e49e56c54..efca4e644 100644 Binary files a/NextcloudTalk/pt-BR.lproj/Localizable.strings and b/NextcloudTalk/pt-BR.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ru.lproj/Localizable.strings b/NextcloudTalk/ru.lproj/Localizable.strings index 4c0e5af96..9f52c5492 100644 Binary files a/NextcloudTalk/ru.lproj/Localizable.strings and b/NextcloudTalk/ru.lproj/Localizable.strings differ diff --git a/NextcloudTalk/sc.lproj/Localizable.strings b/NextcloudTalk/sc.lproj/Localizable.strings index 7e947dd0d..f83bcb455 100644 Binary files a/NextcloudTalk/sc.lproj/Localizable.strings and b/NextcloudTalk/sc.lproj/Localizable.strings differ diff --git a/NextcloudTalk/sk-SK.lproj/Localizable.strings b/NextcloudTalk/sk-SK.lproj/Localizable.strings index 9ea592c3b..e9fefe92c 100644 Binary files a/NextcloudTalk/sk-SK.lproj/Localizable.strings and b/NextcloudTalk/sk-SK.lproj/Localizable.strings differ diff --git a/NextcloudTalk/sl.lproj/Localizable.strings b/NextcloudTalk/sl.lproj/Localizable.strings index c4cc19dc4..2bf1f9a73 100644 Binary files a/NextcloudTalk/sl.lproj/Localizable.strings and b/NextcloudTalk/sl.lproj/Localizable.strings differ diff --git a/NextcloudTalk/sr.lproj/Localizable.strings b/NextcloudTalk/sr.lproj/Localizable.strings index c10bfc7b6..4a3d22f59 100644 Binary files a/NextcloudTalk/sr.lproj/Localizable.strings and b/NextcloudTalk/sr.lproj/Localizable.strings differ diff --git a/NextcloudTalk/sv.lproj/Localizable.strings b/NextcloudTalk/sv.lproj/Localizable.strings index 3bee07283..8d128296b 100644 Binary files a/NextcloudTalk/sv.lproj/Localizable.strings and b/NextcloudTalk/sv.lproj/Localizable.strings differ diff --git a/NextcloudTalk/tr.lproj/Localizable.strings b/NextcloudTalk/tr.lproj/Localizable.strings index 2639ddda8..59ab03e73 100644 Binary files a/NextcloudTalk/tr.lproj/Localizable.strings and b/NextcloudTalk/tr.lproj/Localizable.strings differ diff --git a/NextcloudTalk/ug.lproj/InfoPlist.strings b/NextcloudTalk/ug.lproj/InfoPlist.strings new file mode 100644 index 000000000..f264739e5 Binary files /dev/null and b/NextcloudTalk/ug.lproj/InfoPlist.strings differ diff --git a/NextcloudTalk/vi.lproj/Localizable.strings b/NextcloudTalk/ug.lproj/Localizable.strings similarity index 56% rename from NextcloudTalk/vi.lproj/Localizable.strings rename to NextcloudTalk/ug.lproj/Localizable.strings index b06bfbe10..1a5863268 100644 Binary files a/NextcloudTalk/vi.lproj/Localizable.strings and b/NextcloudTalk/ug.lproj/Localizable.strings differ diff --git a/NextcloudTalk/zh-Hans.lproj/Localizable.strings b/NextcloudTalk/zh-Hans.lproj/Localizable.strings index 62a98af56..d4d92171c 100644 Binary files a/NextcloudTalk/zh-Hans.lproj/Localizable.strings and b/NextcloudTalk/zh-Hans.lproj/Localizable.strings differ diff --git a/NextcloudTalk/zh-Hant-TW.lproj/Localizable.strings b/NextcloudTalk/zh-Hant-TW.lproj/Localizable.strings index 06eae76a9..4b1651992 100644 Binary files a/NextcloudTalk/zh-Hant-TW.lproj/Localizable.strings and b/NextcloudTalk/zh-Hant-TW.lproj/Localizable.strings differ diff --git a/NextcloudTalk/zh_HK.lproj/Localizable.strings b/NextcloudTalk/zh_HK.lproj/Localizable.strings index 3456d671f..e5d2193ec 100644 Binary files a/NextcloudTalk/zh_HK.lproj/Localizable.strings and b/NextcloudTalk/zh_HK.lproj/Localizable.strings differ diff --git a/NextcloudTalkTests/Integration/Helpers.swift b/NextcloudTalkTests/Integration/Helpers.swift index caedcbdd5..30be7f0d2 100644 --- a/NextcloudTalkTests/Integration/Helpers.swift +++ b/NextcloudTalkTests/Integration/Helpers.swift @@ -10,11 +10,11 @@ import Foundation extension XCTestCase { // TODO: This should probably be part of APIController - func getRoomDict(from rawRoomDict: [Any]) -> [NCRoom] { + func getRoomDict(from rawRoomDict: [Any], for account: TalkAccount) -> [NCRoom] { var rooms: [NCRoom] = [] for roomDict in rawRoomDict { - if let roomDict = roomDict as? [AnyHashable: Any] { - rooms.append(NCRoom(dictionary: roomDict)) + if let roomDict = roomDict as? [AnyHashable: Any], let ncRooms = NCRoom(dictionary: roomDict, andAccountId: account.accountId) { + rooms.append(ncRooms) } } @@ -22,20 +22,26 @@ extension XCTestCase { } func checkRoomExists(roomName: String, withAccount account: TalkAccount, completion: ((NCRoom?) -> Void)? = nil) { - let exp = expectation(description: "\(#function)\(#line)") - NCAPIController.sharedInstance().getRooms(forAccount: account, updateStatus: false, modifiedSince: 0) { roomsDict, error in XCTAssertNil(error) - let rooms = self.getRoomDict(from: roomsDict!) + let rooms = self.getRoomDict(from: roomsDict!, for: account) let room = rooms.first(where: { $0.displayName == roomName }) XCTAssertNotNil(room) completion?(room) - - exp.fulfill() } + } - waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + func checkRoomNotExists(roomName: String, withAccount account: TalkAccount, completion: (() -> Void)? = nil) { + NCAPIController.sharedInstance().getRooms(forAccount: account, updateStatus: false, modifiedSince: 0) { roomsDict, error in + XCTAssertNil(error) + + let rooms = self.getRoomDict(from: roomsDict!, for: account) + let room = rooms.first(where: { $0.displayName == roomName }) + XCTAssertNil(room) + + completion?() + } } } diff --git a/NextcloudTalkTests/Integration/IntegrationRoomTest.swift b/NextcloudTalkTests/Integration/IntegrationRoomTest.swift index 0b303011d..298848f59 100644 --- a/NextcloudTalkTests/Integration/IntegrationRoomTest.swift +++ b/NextcloudTalkTests/Integration/IntegrationRoomTest.swift @@ -25,7 +25,7 @@ final class IntegrationRoomTest: TestBase { waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) } - func testRoomCreation() throws { + func testRoomCreationAndDeletion() throws { let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() let roomName = "Integration Test Room " + UUID().uuidString @@ -34,12 +34,21 @@ final class IntegrationRoomTest: TestBase { // Create a room NCAPIController.sharedInstance().createRoom(forAccount: activeAccount, withInvite: nil, ofType: .public, andName: roomName) { _, error in XCTAssertNil(error) - exp.fulfill() + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + + // Delete the room again + NCAPIController.sharedInstance().deleteRoom(room!.token, forAccount: activeAccount) { error in + XCTAssertNil(error) + + self.checkRoomNotExists(roomName: roomName, withAccount: activeAccount) { + exp.fulfill() + } + } + } } waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) - - self.checkRoomExists(roomName: roomName, withAccount: activeAccount) } func testRoomDescription() throws { @@ -67,14 +76,14 @@ final class IntegrationRoomTest: TestBase { // Set a description NCAPIController.sharedInstance().setRoomDescription(roomDescription, forRoom: roomToken, forAccount: activeAccount) { error in XCTAssertNil(error) - expDescription.fulfill() + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + XCTAssertEqual(room?.roomDescription, roomDescription) + expDescription.fulfill() + } } waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) - - self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in - XCTAssertEqual(room?.roomDescription, roomDescription) - } } func testRoomRename() throws { @@ -102,13 +111,153 @@ final class IntegrationRoomTest: TestBase { // Set a new name NCAPIController.sharedInstance().renameRoom(roomToken, forAccount: activeAccount, withName: roomNameNew) { error in XCTAssertNil(error) - expNewName.fulfill() + + self.checkRoomExists(roomName: roomNameNew, withAccount: activeAccount) { room in + XCTAssertEqual(room?.displayName, roomNameNew) + expNewName.fulfill() + } + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + } + + func testRoomPublicPrivate() throws { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + let roomName = "PublicPrivate Test Room " + UUID().uuidString + + var exp = expectation(description: "\(#function)\(#line)") + var roomToken = "" + + // Create a room + NCAPIController.sharedInstance().createRoom(forAccount: activeAccount, withInvite: nil, ofType: .group, andName: roomName) { room, error in + XCTAssertNil(error) + + roomToken = room?.token ?? "" + XCTAssert(room?.type == .group) + + exp.fulfill() + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + + // Make room public + exp = expectation(description: "\(#function)\(#line)") + NCAPIController.sharedInstance().makeRoomPublic(roomToken, forAccount: activeAccount) { error in + XCTAssertNil(error) + + NCAPIController.sharedInstance().getRoom(forAccount: activeAccount, withToken: roomToken) { roomDict, error in + XCTAssertNil(error) + + let room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) + XCTAssertNotNil(room) + XCTAssert(room?.type == .public) + + exp.fulfill() + } + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + + // Make room private again + exp = expectation(description: "\(#function)\(#line)") + NCAPIController.sharedInstance().makeRoomPrivate(roomToken, forAccount: activeAccount) { error in + XCTAssertNil(error) + + NCAPIController.sharedInstance().getRoom(forAccount: activeAccount, withToken: roomToken) { roomDict, error in + XCTAssertNil(error) + + let room = NCRoom(dictionary: roomDict, andAccountId: activeAccount.accountId) + XCTAssertNotNil(room) + XCTAssert(room?.type == .group) + + exp.fulfill() + } + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + } + + func testRoomPassword() throws { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + let roomName = "Password Test Room " + UUID().uuidString + + var exp = expectation(description: "\(#function)\(#line)") + var roomToken = "" + + // Create a room + NCAPIController.sharedInstance().createRoom(forAccount: activeAccount, withInvite: nil, ofType: .public, andName: roomName) { room, error in + XCTAssertNil(error) + + roomToken = room?.token ?? "" + + exp.fulfill() + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + + // Set a password + exp = expectation(description: "\(#function)\(#line)") + NCAPIController.sharedInstance().setPassword("1234", forRoom: roomToken, forAccount: activeAccount) { error, _ in + XCTAssertNil(error) + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + XCTAssertTrue(room?.hasPassword ?? false) + + // Remove password again + NCAPIController.sharedInstance().setPassword("", forRoom: roomToken, forAccount: activeAccount) { error, _ in + XCTAssertNil(error) + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + XCTAssertFalse(room?.hasPassword ?? true) + + exp.fulfill() + } + } + } + } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) + } + + func testRoomFavorite() throws { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + let roomName = "Favorite Test Room " + UUID().uuidString + + var exp = expectation(description: "\(#function)\(#line)") + var roomToken = "" + + // Create a room + NCAPIController.sharedInstance().createRoom(forAccount: activeAccount, withInvite: nil, ofType: .public, andName: roomName) { room, error in + XCTAssertNil(error) + + roomToken = room?.token ?? "" + + exp.fulfill() } waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) - self.checkRoomExists(roomName: roomNameNew, withAccount: activeAccount) { room in - XCTAssertEqual(room?.displayName, roomNameNew) + // Set as favorite + exp = expectation(description: "\(#function)\(#line)") + NCAPIController.sharedInstance().addRoomToFavorites(roomToken, forAccount: activeAccount) { error in + XCTAssertNil(error) + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + XCTAssertTrue(room?.isFavorite ?? false) + + // Remove from favorite + NCAPIController.sharedInstance().removeRoomFromFavorites(roomToken, forAccount: activeAccount) { error in + XCTAssertNil(error) + + self.checkRoomExists(roomName: roomName, withAccount: activeAccount) { room in + XCTAssertFalse(room?.isFavorite ?? true) + + exp.fulfill() + } + } + } } + + waitForExpectations(timeout: TestConstants.timeoutLong, handler: nil) } } diff --git a/NextcloudTalkTests/UI/UIRoomTest.swift b/NextcloudTalkTests/UI/UIRoomTest.swift index 5d40759cf..71cc0d6c4 100644 --- a/NextcloudTalkTests/UI/UIRoomTest.swift +++ b/NextcloudTalkTests/UI/UIRoomTest.swift @@ -128,8 +128,8 @@ final class UIRoomTest: XCTestCase { textView.typeText("M") textView.typeText("e") - let predicate = NSPredicate(format: "label CONTAINS[c] %@", newConversationName) - let autoCompleteCell = app.tables.cells["AutoCompletionCellIdentifier"].staticTexts.containing(predicate).firstMatch + let predicateLabel = NSPredicate(format: "label CONTAINS[c] %@", newConversationName) + let autoCompleteCell = app.tables.cells["AutoCompletionCellIdentifier"].staticTexts.containing(predicateLabel).firstMatch XCTAssert(autoCompleteCell.waitForExistence(timeout: TestConstants.timeoutShort)) autoCompleteCell.tap() @@ -142,6 +142,28 @@ final class UIRoomTest: XCTestCase { // Check if the input field is now empty XCTAssertEqual(textView.value as? String ?? "", "") + + // Try to send a mention and check if it's rendered + textView.tap() + textView.typeText("@") + textView.typeText("M") + textView.typeText("e") + + XCTAssert(autoCompleteCell.waitForExistence(timeout: TestConstants.timeoutShort)) + + autoCompleteCell.tap() + + // Check if the mention was correctly inserted in the textView + XCTAssertEqual(textView.value as? String ?? "", "@\(newConversationName) ") + + let sendMessageButton = toolbar.buttons["Send message"] + sendMessageButton.tap() + + // Wait for temporary message to be replaced + XCTAssert(app.images["MessageSent"].waitForExistence(timeout: TestConstants.timeoutShort)) + + let tables = app.tables + XCTAssert(tables.textViews["@" + newConversationName].waitForExistence(timeout: TestConstants.timeoutShort)) } func testLobbyView() { diff --git a/NextcloudTalkTests/Unit/Chat/UnitBaseChatTableViewCellTest.swift b/NextcloudTalkTests/Unit/Chat/UnitBaseChatTableViewCellTest.swift index c14690d4b..5407c5a1f 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitBaseChatTableViewCellTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitBaseChatTableViewCellTest.swift @@ -59,10 +59,10 @@ final class UnitBaseChatTableViewCellTest: TestBaseRealm { } let deckCell: BaseChatTableViewCell = .fromNib() - deckCell.setup(for: deckMessage, inRoom: room) + deckCell.setup(for: deckMessage, inRoom: room, withAccount: activeAccount) let quoteCell: BaseChatTableViewCell = .fromNib() - quoteCell.setup(for: quoteMessage, inRoom: room) + quoteCell.setup(for: quoteMessage, inRoom: room, withAccount: activeAccount) } } diff --git a/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift b/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift index 8e549c479..f5241d84e 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift @@ -38,19 +38,9 @@ final class UnitBaseChatViewControllerTest: TestBaseRealm { override func setUpWithError() throws { try super.setUpWithError() - baseController = BaseChatViewController(for: NCRoom())! - testMessage = NCChatMessage() - } - - func testInvisibleCellHeight() throws { - // Empty message should have a height of 0 - testMessage.message = "" - XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 0.0) - - // Test update message - testMessage.message = "System Message" - testMessage.systemMessage = "message_deleted" - XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 0.0) + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + baseController = BaseChatViewController(forRoom: NCRoom(), withAccount: activeAccount)! + testMessage = NCChatMessage(dictionary: [:], andAccountId: activeAccount.accountId) } func testSystemMessageCellHeight() throws { diff --git a/NextcloudTalkTests/Unit/Chat/UnitChatViewControllerTest.swift b/NextcloudTalkTests/Unit/Chat/UnitChatViewControllerTest.swift index 35101c76c..2d6bc31ec 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitChatViewControllerTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitChatViewControllerTest.swift @@ -8,7 +8,7 @@ import XCTest final class UnitChatViewControllerTest: TestBaseRealm { - func testLocalMention() throws { + func testExpireMessages() throws { let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() let roomName = "Expire Messages Test Room" let roomToken = "expToken" @@ -51,7 +51,7 @@ final class UnitChatViewControllerTest: TestBaseRealm { XCTAssertEqual(NCChatMessage.allObjects().count, 2) - let chatViewController = ChatViewController(for: room)! + let chatViewController = ChatViewController(forRoom: room, withAccount: activeAccount)! let messageArray = [expMessage1, expMessage2].map { NCChatMessage(value: $0) } chatViewController.appendMessages(messages: messageArray) @@ -69,6 +69,50 @@ final class UnitChatViewControllerTest: TestBaseRealm { XCTAssertEqual(NCChatMessage.allObjects().count, 0) } + func testJoinRoomWithEmptyRoomObject() throws { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + let roomName = "EmptyRoomObject" + let roomToken = "emptyRoomObject" + + let room = NCRoom() + room.token = roomToken + room.name = roomName + room.accountId = activeAccount.accountId + + try? realm.transaction { + realm.add(room) + } + + let chatViewController = ChatViewController(forRoom: room, withAccount: activeAccount)! + + expectation(forNotification: .NCRoomsManagerDidJoinRoom, object: nil) { notification -> Bool in + XCTAssertNil(notification.userInfo?["error"]) + + // swiftlint:disable:next force_cast + XCTAssertEqual(notification.userInfo?["token"] as! String, roomToken) + + return true + } + + let userInfo: [String: Any] = [ + "token": roomToken, + "room": NCRoom() + ] + + NotificationCenter.default.post(name: .NCRoomsManagerDidJoinRoom, object: self, userInfo: userInfo) + + waitForExpectations(timeout: TestConstants.timeoutShort, handler: nil) + + let exp = expectation(description: "\(#function)\(#line)") + + DispatchQueue.main.async { + XCTAssertNotNil(chatViewController.room.token) + exp.fulfill() + } + + waitForExpectations(timeout: TestConstants.timeoutShort, handler: nil) + } + func testFrequentlyEmojis() throws { var activeAccount = NCDatabaseManager.sharedInstance().activeAccount() XCTAssertEqual(activeAccount.frequentlyUsedEmojis, ["👍", "❤️", "😂", "😅"]) diff --git a/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift b/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift index f619a1a38..b456ed7b4 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift @@ -10,7 +10,7 @@ final class UnitNCChatMessageTest: TestBaseRealm { func testUnreadMessageSeparatorUrlCheck() throws { let message = NCChatMessage() - message.messageId = kUnreadMessagesSeparatorIdentifier + message.messageId = MessageSeparatorTableViewCell.unreadMessagesSeparatorId updateCapabilities { cap in cap.referenceApiSupported = true @@ -19,4 +19,35 @@ final class UnitNCChatMessageTest: TestBaseRealm { XCTAssertFalse(message.containsURL()) } + func testMentionRendering() throws { + let mentionParameters = """ + { + "mention-user1": { + "type": "user", + "id": "username@nextcloud.invalid", + "name": "Username with space", + "server": "https://nextcloud.invalid" + } + } + """ + + let mentionMessage = NCChatMessage() + mentionMessage.messageParametersJSONString = mentionParameters + + mentionMessage.message = "{mention-user1}" + XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, "@Username with space") + + mentionMessage.message = "{\n{mention-user1}" + XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, "{\n@Username with space") + + mentionMessage.message = "@{mention-user1}" + XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, "@@Username with space") + + mentionMessage.message = " abc{mention-user1}abc " + XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, " abc@Username with spaceabc ") + + mentionMessage.message = "{mention-user1}{mention-user2}" + XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, "@Username with space{mention-user2}") + } + } diff --git a/ShareExtension/ShareConfirmationViewController.swift b/ShareExtension/ShareConfirmationViewController.swift index 26b5aa56a..0d69cc459 100644 --- a/ShareExtension/ShareConfirmationViewController.swift +++ b/ShareExtension/ShareConfirmationViewController.swift @@ -12,8 +12,9 @@ import AVFoundation import MBProgressHUD @objc public protocol ShareConfirmationViewControllerDelegate { - @objc func shareConfirmationViewControllerDidFailed(_ viewController: ShareConfirmationViewController) + @objc func shareConfirmationViewControllerDidFail(_ viewController: ShareConfirmationViewController) @objc func shareConfirmationViewControllerDidFinish(_ viewController: ShareConfirmationViewController) + @objc func shareConfirmationViewControllerDidCancel(_ viewController: ShareConfirmationViewController) } @objcMembers public class ShareConfirmationViewController: InputbarViewController, @@ -29,7 +30,6 @@ import MBProgressHUD // MARK: - Public var - public var account: TalkAccount public var isModal: Bool = false public var forwardingMessage: Bool = false @@ -219,10 +219,9 @@ import MBProgressHUD // MARK: - Init. public init?(room: NCRoom, account: TalkAccount, serverCapabilities: ServerCapabilities) { - self.account = account self.serverCapabilities = serverCapabilities - super.init(for: room, withView: self.shareContentView) + super.init(forRoom: room, withAccount: account, withView: self.shareContentView) self.shareContentView.addSubview(self.toLabelView) NSLayoutConstraint.activate([ @@ -420,7 +419,7 @@ import MBProgressHUD } func cancelButtonPressed() { - self.delegate?.shareConfirmationViewControllerDidFinish(self) + self.delegate?.shareConfirmationViewControllerDidCancel(self) } func sendButtonPressed() { @@ -516,7 +515,7 @@ import MBProgressHUD NCAPIController.sharedInstance().sendChatMessage(self.shareTextView.text, toRoom: self.room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: self.account) { error in if let error { NCUtils.log(String(format: "Failed to share text. Error: %@", error.localizedDescription)) - self.delegate?.shareConfirmationViewControllerDidFailed(self) + self.delegate?.shareConfirmationViewControllerDidFail(self) } else { NCIntentController.sharedInstance().donateSendMessageIntent(for: self.room) self.delegate?.shareConfirmationViewControllerDidFinish(self) @@ -530,7 +529,7 @@ import MBProgressHUD NCAPIController.sharedInstance().shareRichObject(self.objectShareMessage?.richObjectFromObjectShare, inRoom: self.room.token, for: self.account) { error in if let error { NCUtils.log(String(format: "Failed to share rich object. Error: %@", error.localizedDescription)) - self.delegate?.shareConfirmationViewControllerDidFailed(self) + self.delegate?.shareConfirmationViewControllerDidFail(self) } else { NCIntentController.sharedInstance().donateSendMessageIntent(for: self.room) self.delegate?.shareConfirmationViewControllerDidFinish(self) diff --git a/ShareExtension/ShareViewController.m b/ShareExtension/ShareViewController.m index 7b2e8e62b..ae332712a 100644 --- a/ShareExtension/ShareViewController.m +++ b/ShareExtension/ShareViewController.m @@ -505,7 +505,7 @@ - (NSArray *)filterRoomsWithString:(NSString *)searchString #pragma mark - ShareConfirmationViewController Delegate -- (void)shareConfirmationViewControllerDidFailed:(ShareConfirmationViewController *)viewController +- (void)shareConfirmationViewControllerDidFail:(ShareConfirmationViewController *)viewController { [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; } @@ -515,6 +515,10 @@ - (void)shareConfirmationViewControllerDidFinish:(ShareConfirmationViewControlle [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; } +- (void)shareConfirmationViewControllerDidCancel:(ShareConfirmationViewController *)viewController +{ + [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; +} #pragma mark - Table view data source