Merge commit '56d9e1f6a55a93dc71149ae429eaa615a98de0d5' into feature/6076_foss_merge

* commit '56d9e1f6a55a93dc71149ae429eaa615a98de0d5': (79 commits)
  finish version++
  version++
  Translated using Weblate (Hungarian)
  Translated using Weblate (Italian)
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Hungarian)
  Translated using Weblate (Slovak)
  Translated using Weblate (Swedish)
  Translated using Weblate (Indonesian)
  Translated using Weblate (Albanian)
  Translated using Weblate (Estonian)
  Translated using Weblate (Estonian)
  updated the submodule
  updated SDK
  Update the SDK. (#7819)
  Prepare for new sprint
  finish version++
  version++
  fix
  Changelog.
  ...

# Conflicts:
#	Config/AppVersion.xcconfig
#	Podfile
#	Podfile.lock
#	Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
#	Riot/Managers/Settings/RiotSettings.swift
#	Riot/Modules/Analytics/Analytics.swift
#	Riot/Modules/Analytics/DecryptionFailure.swift
#	Riot/Modules/Analytics/PHGPostHogConfiguration.swift
#	Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift
#	Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift
#	Riot/Modules/Room/Views/Title/Preview/PreviewRoomTitleView.m
#	Riot/Modules/Settings/SettingsViewController.m
#	Riot/Utils/EventFormatter.m
#	Riot/Utils/Tools.m
#	RiotNSE/target.yml
#	fastlane/Fastfile
#	project.yml
This commit is contained in:
JanNiklas Grabowski
2024-08-19 12:52:38 +02:00
75 changed files with 1900 additions and 213 deletions

View File

@@ -20,7 +20,9 @@ jobs:
# Concurrency group not needed as this workflow only runs on develop which we always want to test.
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: 'true'
# Common cache
# Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job
@@ -49,8 +51,6 @@ jobs:
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Use right MatrixSDK versions
run: bundle exec fastlane point_dependencies_to_related_branches
# Main step
- name: Build iOS simulator

View File

@@ -25,7 +25,9 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: 'true'
# Common cache
# Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job
@@ -54,8 +56,6 @@ jobs:
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Use right MatrixSDK versions
run: bundle exec fastlane point_dependencies_to_related_branches
# Main step
- name: Unit tests

View File

@@ -20,7 +20,9 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: 'true'
# Common cache
# Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job
@@ -49,8 +51,6 @@ jobs:
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Use right MatrixSDK versions
run: bundle exec fastlane point_dependencies_to_related_branches
# Main step
- name: UI tests

View File

@@ -25,7 +25,9 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: 'true'
# Common cache
# Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job
@@ -57,8 +59,6 @@ jobs:
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Use right MatrixSDK versions
run: bundle exec fastlane point_dependencies_to_related_branches
# Import alpha release private signing certificate
- name: Import signing certificate

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "matrix-ios-sdk"]
path = matrix-ios-sdk
url = git@github.com:matrix-org/matrix-ios-sdk.git

View File

@@ -1,3 +1,50 @@
## Changes in 1.11.16 (2024-07-23)
No significant changes.
## Changes in 1.11.15 (2024-06-18)
No significant changes.
## Changes in 1.11.14 (2024-06-17)
🙌 Improvements
- Room retention event implementation ([#7809](https://github.com/element-hq/element-ios/pull/7809))
## Changes in 1.11.13 (2024-06-12)
Others
- Analytics | Add support for super properties and appPlatform ([#7801](https://github.com/element-hq/element-ios/issues/7801))
## Changes in 1.11.12 (2024-05-30)
🐛 Bugfixes
- Fix a crash when the user taps play multiple times and the video download fails. ([#7791](https://github.com/element-hq/element-ios/issues/7791))
## Changes in 1.11.11 (2024-05-29)
No significant changes.
## Changes in 1.11.10 (2024-05-01)
🙌 Improvements
- Upgrade MatrixSDK version ([v0.27.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.27.7)).
Others
- Improvements to reporting of decryption failures.
## Changes in 1.11.9 (2024-04-02)
Others

View File

@@ -297,7 +297,6 @@ final class BuildSettings: NSObject {
static let settingsScreenShowChangePassword:Bool = true
static let settingsScreenShowEnableStunServerFallback: Bool = true
static let settingsScreenShowNotificationDecodedContentOption: Bool = true
static let settingsScreenShowNsfwRoomsOption: Bool = true
static let settingsSecurityScreenShowSessions:Bool = true
static let settingsSecurityScreenShowSetupBackup:Bool = true
static let settingsSecurityScreenShowRestoreBackup:Bool = true

View File

@@ -5,7 +5,7 @@ GEM
base64
nkf
rexml
activesupport (7.1.3.2)
activesupport (7.1.3.4)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -23,24 +23,24 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.899.0)
aws-sdk-core (3.191.4)
aws-partitions (1.941.0)
aws-sdk-core (3.197.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.78.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.83.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-s3 (1.152.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
bigdecimal (3.1.7)
bigdecimal (3.1.8)
claide (1.1.0)
clamp (1.3.2)
cocoapods (1.14.3)
@@ -84,7 +84,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.2.3)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
declarative (0.0.20)
digest-crc (0.6.5)
@@ -125,15 +125,15 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.0)
fastlane (2.219.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -154,10 +154,10 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
@@ -166,14 +166,14 @@ GEM
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-brew (0.1.1)
fastlane-plugin-sentry (1.20.0)
fastlane-plugin-sentry (1.23.0)
os (~> 1.1, >= 1.1.4)
fastlane-plugin-versioning (0.5.2)
fastlane-plugin-xcodegen (1.1.0)
fastlane-plugin-brew (~> 0.1.1)
ffi (1.16.3)
ffi (1.17.0)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@@ -214,22 +214,22 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.4)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.7.1)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.22.3)
mini_portile2 (2.8.7)
minitest (5.23.1)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.0)
multipart-post (2.4.1)
mutex_m (0.2.0)
nanaimo (0.3.0)
nap (1.1.0)
@@ -239,23 +239,24 @@ GEM
nokogiri (1.15.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
optparse (0.4.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (4.0.7)
racc (1.7.3)
rake (13.1.0)
racc (1.8.0)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@@ -270,6 +271,7 @@ GEM
clamp (~> 1.3)
nokogiri (>= 1.14.3)
xcodeproj (~> 1.21)
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)

View File

@@ -1,5 +1,32 @@
{
"pins" : [
{
"identity" : "bluecryptor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/BlueCryptor.git",
"state" : {
"revision" : "cec97c24b111351e70e448972a7d3fe68a756d6d",
"version" : "2.0.2"
}
},
{
"identity" : "blueecc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/BlueECC.git",
"state" : {
"revision" : "1485268a54f8135435a825a855e733f026fa6cc8",
"version" : "1.2.201"
}
},
{
"identity" : "bluersa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/BlueRSA.git",
"state" : {
"revision" : "440f78db26d8bb073f29590f1c7bd31004da09ae",
"version" : "1.0.201"
}
},
{
"identity" : "devicekit",
"kind" : "remoteSourceControl",
@@ -27,6 +54,24 @@
"version" : "1.7.18"
}
},
{
"identity" : "kituracontracts",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/KituraContracts.git",
"state" : {
"revision" : "6edf7ac3dd2b3a2c61284778d430bbad7d8a6f23",
"version" : "2.0.1"
}
},
{
"identity" : "loggerapi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/LoggerAPI.git",
"state" : {
"revision" : "4e6b45e850ffa275e8e26a24c6454fd709d5b6ac",
"version" : "2.0.0"
}
},
{
"identity" : "lottie-ios",
"kind" : "remoteSourceControl",
@@ -86,8 +131,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
"version" : "1.1.0"
}
},
{
"identity" : "swift-jwt",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kitura/Swift-JWT",
"state" : {
"revision" : "08e02ff214c41df49bdd189ff837d68ba11c437b",
"version" : "4.0.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
}
},
{

View File

@@ -592,6 +592,8 @@ Tap the + to start adding people.";
"room_action_send_sticker" = "Send sticker";
"room_action_send_file" = "Send file";
"room_action_reply" = "Reply";
"room_action_report" = "Report room";
"room_action_report_prompt_reason" = "Reason for reporting this room";
"room_replacement_information" = "This room has been replaced and is no longer active.";
"room_replacement_link" = "The conversation continues here.";
"room_predecessor_information" = "This room is a continuation of another conversation.";
@@ -2603,6 +2605,8 @@ To enable access, tap Settings> Location and select Always";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Shrink composer";
"wysiwyg_composer_action_maximise_action" = "Expand composer";
// Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Photo Library";

View File

@@ -2719,3 +2719,10 @@
"settings_manage_account_description" = "Halda kasutajakontot koduserveris %@";
"manage_session_redirect" = "Järgmiseks suuname sind sinu serveriteenuse autentijale ning seal saad sa väljalogimise lõpuni viia.";
"manage_session_redirect_error" = "See funktsionaalsus pole hetkel saadaval. Lisateavet saad oma koduserveri haldajalt";
"room_action_report" = "Teata jututoast";
"room_action_report_prompt_reason" = "Jututoast teatamise põhjus";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Peida tekstitoimeti";
"wysiwyg_composer_action_maximise_action" = "Ava tekstitoimeti";

View File

@@ -97,7 +97,7 @@
"contacts_address_book_matrix_users_toggle" = "فقط کاربران ماتریس";
"directory_search_results_more_than" = ">%tu نتیجه برای %@ یافت شد";
"directory_search_results" = "%tu نتیجه برای %@ یافت شد";
"search_in_progress" = "در حال جست وجو";
"search_in_progress" = "جوییدن…";
"search_no_result" = "نتیچه ای یافت نشد";
"search_people_placeholder" = "جستجو بر اساس شناسه کاربری، نام یا آدرس ایمیل";
"search_default_placeholder" = "جست و جو";
@@ -246,7 +246,7 @@
"room_accessibility_search" = "جستجو";
"room_message_edits_history_title" = "ویرایش های پیام";
"room_resource_usage_limit_reached_message_2" = "برخی از کاربران نمی توانند وارد سیستم شوند.";
"room_resource_limit_exceeded_message_contact_3" = " برای ادامه استفاده از این سرویس";
"room_resource_limit_exceeded_message_contact_3" = " برای ادامه استفاده از این خدمت.";
"room_resource_limit_exceeded_message_contact_2_link" = "با سرپرست سرویس خود تماس بگیرید";
"room_resource_limit_exceeded_message_contact_1" = " لطفا ";
"room_predecessor_link" = "برای دیدن پیام‌های قدیمی‌تر به اینجاضربه بزنید.";
@@ -416,7 +416,7 @@
"event_formatter_call_retry" = "تلاش مجدد";
"event_formatter_call_answer" = "پاسخ";
"event_formatter_call_decline" = "رد تماس";
"event_formatter_call_back" = "تماس";
"event_formatter_call_back" = "پاسخ تماس";
"event_formatter_call_connection_failed" = "ارتباط ناموفق بود";
"event_formatter_call_missed_video" = "تماس ویدیویی از دست رفته";
"event_formatter_call_missed_voice" = "تماس صوتی از دست رفته";
@@ -464,7 +464,7 @@
"group_participants_invited_section" = "دعوت شده";
"group_participants_invite_malformed_id_title" = "خطای دعوت";
"group_participants_invite_another_user" = "جستجو / دعوت با شناسه کاربری یا نام";
"group_participants_filter_members" = "اعضای انجمن را فیلتر کنید";
"group_participants_filter_members" = "پالایش اعضای اجتماع";
"group_participants_invite_prompt_msg" = "آیا مطمئنید که می خواهید %@ را به این گروه دعوت کنید؟";
"group_participants_invite_prompt_title" = "تایید";
"group_participants_remove_prompt_msg" = "آیا مطمئنید که می خواهید %@ را از این گروه حذف کنید؟";
@@ -570,7 +570,7 @@
"room_details_files" = "بارگذاری شده ها";
"room_details_people" = "اعضا";
"room_details_title_for_dm" = "جزییات";
"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "به هر حال قطع کن";
"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "قطع شدن به هر روی";
"identity_server_settings_alert_disconnect_button" = "قطع کردن";
"identity_server_settings_disconnect" = "قطع کردن";
"identity_server_settings_change" = "تغییر دادن";
@@ -652,7 +652,7 @@
"settings_confirm_password" = "تایید رمز";
"settings_new_password" = "رمز جدید";
"settings_old_password" = "رمز قدیمی";
"settings_third_party_notices" = "اعلامیه های شخص ثالث";
"settings_third_party_notices" = "یادآوری‌های سوم‌شخص";
"settings_privacy_policy" = "سیاست حفظ حریم خصوصی";
"settings_term_conditions" = "شرایط و ضوابط";
"settings_copyright" = "کپی رایت";
@@ -684,7 +684,7 @@
"settings_room_upgrades" = "ارتقاء اتاق";
"settings_messages_by_a_bot" = "پیام های ربات";
"settings_call_invitations" = "دعوت نامه های تماس";
"settings_room_invitations" = "دعوت نامه های اتاق";
"settings_room_invitations" = "دعوتهای اتاق";
"settings_messages_containing_keywords" = "کلمات کلیدی";
"settings_messages_containing_at_room" = "@ اتاق";
"settings_messages_containing_user_name" = "نام کاربری من";
@@ -694,13 +694,13 @@
"settings_encrypted_direct_messages" = "پیام های مستقیم رمزگذاری شده";
"settings_direct_messages" = "پیام مستقیم";
"settings_notify_me_for" = "به من اطلاع بده برای";
"settings_mentions_and_keywords" = "ذکر و کلمات کلیدی";
"settings_mentions_and_keywords" = "نام‌بری و کلیدواژگان";
"settings_default" = "اعلان های پیش فرض";
"settings_notifications_disabled_alert_message" = "برای فعال کردن اعلان‌ها، به تنظیمات دستگاه خود بروید.";
"settings_notifications_disabled_alert_title" = "غیر فعال کردن اعلان ها";
"settings_pin_rooms_with_missed_notif" = "پین کردن اتاق هایی با اعلان های از دست رفته";
"settings_pin_rooms_with_unread" = "پین کردن اتاق هایی با پیام های خوانده نشده";
"settings_global_settings_info" = "تنظیمات اعلان جهانی در سرویس گیرنده وب %@ شما موجود است";
"settings_global_settings_info" = "تنظیمات آگاهی عمومی در کارخواه وب %@تان موجود است";
"settings_show_decrypted_content" = "نمایش محتوای رمزگشایی شده";
"settings_device_notifications" = "اعلان های دستگاه";
"settings_enable_push_notif" = "اعلان ها در این دستگاه";
@@ -1346,3 +1346,4 @@
"onboarding_splash_page_2_title" = "تحت کنترل شماست.";
"onboarding_splash_page_1_message" = "یک ارتباط امن و مستقل که سطح حریم شخصی آن دقیقا مشابه ارتباط رو در رو در منزل شماست.";
"accessibility_selected" = "انتخاب شده";
"room_title_members" = "%@ عضو";

View File

@@ -1352,7 +1352,7 @@
"event_formatter_call_ringing" = "Hívás…";
"event_formatter_call_connecting" = "Kapcsolás…";
"settings_labs_enable_ringing_for_group_calls" = "Csengetés csoportos hívásokhoz";
"room_no_privileges_to_create_group_call" = "Adminisztrátornak vagy moderátornak kell lenned a hívás indításához.";
"room_no_privileges_to_create_group_call" = "Adminisztrátornak vagy moderátornak kell lennie a hívás indításához.";
"room_join_group_call" = "Csatlakozás";
// Chat
@@ -2478,9 +2478,9 @@
"user_sessions_settings" = "Munkamenetek kezelése";
"invite_to" = "Meghívó ide: %@";
"device_name_unknown" = "Ismeretlen kliens";
"device_name_mobile" = "%@ Mobil";
"device_name_desktop" = "%@ Alkalmazás";
"device_name_web" = "%@ Web";
"device_name_mobile" = "Mobilos %@";
"device_name_desktop" = "Asztali %@";
"device_name_web" = "Webes %@";
"user_session_item_details" = "%1$@ · %2$@";
// First item is client name and second item is session display name
@@ -2501,8 +2501,8 @@
"room_first_message_placeholder" = "Küld el az első üzenetedet…";
"authentication_qr_login_confirm_title" = "Biztonságos kapcsolat beállítva";
"room_event_encryption_info_key_authenticity_not_guaranteed" = "A titkosított üzenetek valódiságát ezen az eszközön nem lehet garantálni.";
"wysiwyg_composer_format_action_underline" = "Aláhúzott";
"wysiwyg_composer_format_action_strikethrough" = "Áthúzott";
"wysiwyg_composer_format_action_underline" = "Aláhúzott formázás alkalmazása";
"wysiwyg_composer_format_action_strikethrough" = "Áthúzott formázás alkalmazása";
"wysiwyg_composer_format_action_italic" = "Dőlt";
// Formatting Actions
@@ -2528,19 +2528,19 @@
"user_session_details_device_os" = "Operációs rendszer";
"user_session_details_device_browser" = "Böngésző";
"user_session_details_device_model" = "Modell";
"user_session_details_device_ip_location" = "Tartózkodási helyem";
"user_session_details_device_ip_location" = "Saját tartózkodási hely";
"user_session_details_device_ip_address" = "IP cím";
"user_session_details_last_activity" = "Utolsó tevékenység";
"user_session_details_session_section_footer" = "A másoláshoz koppints és tartsd rajta az ujjad.";
"user_session_details_session_id" = "Kapcsolat azonosító";
"user_session_details_session_section_footer" = "A másoláshoz koppintson és tartsa lenyomva.";
"user_session_details_session_id" = "Kapcsolatazonosító";
"user_session_details_session_name" = "Munkamenet neve";
"user_session_details_device_section_header" = "Eszköz";
"user_session_details_application_section_header" = "Alkalmazás";
"user_session_details_session_section_header" = "Munkamenet";
"user_session_details_title" = "Munkamenet információk";
"user_session_details_title" = "Munkamenet-információk";
"device_type_name_unknown" = "Ismeretlen";
"device_type_name_mobile" = "Mobil";
"device_type_name_web" = "Web";
"device_type_name_mobile" = "Mobilos";
"device_type_name_web" = "Webes";
"device_type_name_desktop" = "Asztali";
"user_inactive_session_item_with_date" = "90+ napja inaktív (%@)";
"user_inactive_session_item" = "90+ napja inaktív";
@@ -2740,3 +2740,37 @@
"key_verification_self_verify_security_upgrade_alert_title" = "Alkalmazás frissítve";
"settings_acceptable_use" = "Elfogadható felhasználói feltételek";
"room_creation_user_not_found_prompt_title" = "Megerősítés";
"room_creation_user_not_found_prompt_message" = "Nem található profil ehhez a Matrix-azonosítóhoz. Mindenképp elindítja a privát csevegést?";
"room_creation_user_not_found_prompt_invite_action" = "Privát csevegés mindenképp";
"room_participants_invite_unknown_participant_prompt_to_msg" = "Nem található profil ehhez a Matrix-azonosítóhoz. Mindenképp meghívja %@ felhasználót a(z) %@ szobába?";
"room_participants_invite_anyway" = "Meghívás mindenképp";
// Room commands descriptions
"room_command_change_display_name_description" = "Megváltoztatja a megjelenítendő becenevét";
"room_command_emote_description" = "Megjeleníti a műveletet";
"room_command_join_room_description" = "Csatlakozik a megadott című szobához";
"room_command_part_room_description" = "Elhagyja a szobát";
"room_command_invite_user_description" = "Meghívja az adott azonosítójú felhasználót a jelenlegi szobába";
"room_command_kick_user_description" = "Eltávolítja az adott azonosítójú felhasználót ebből a szobából";
"room_command_ban_user_description" = "Kitiltja az adott azonosítójú felhasználót";
"room_command_unban_user_description" = "Feloldja az adott azonosítójú felhasználó kitiltását";
"notice_display_name_changed_to" = "%@ erre módosította a megjelenítendő nevét: %@";
"poll_timeline_loading" = "Betöltés...";
"manage_session_redirect" = "Át lesz irányítva a kiszolgálója hitelesítési szolgáltatójához, hogy befejezze a kijelentkezést.";
"manage_session_redirect_error" = "A funkcionalitás jelenleg nem érhető el. Lépjen kapcsolatba a Matrix-kiszolgáló rendszergazdájával.";
"settings_manage_account_title" = "Fiók";
"settings_manage_account_action" = "Fiók kezelése";
"settings_manage_account_description" = "A fiókja kezelése itt: %@";
"room_command_change_room_topic_description" = "Beállítja a szoba témáját";
"room_action_report" = "Szoba jelentése";
"room_action_report_prompt_reason" = "A szoba jelentésének oka";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Szerkesztő összecsukása";
"wysiwyg_composer_action_maximise_action" = "Szerkesztő kibontása";
"room_command_set_user_power_level_description" = "Meghatározza a felhasználó szintjét";
"room_command_reset_user_power_level_description" = "Elveszi az adott azonosítójú felhasználó operátori jogosultságát";
"room_command_error_unknown_command" = "Érvénytelen vagy nem kezelt parancs";
"room_command_discard_session_description" = "Kényszeríti a titkosított szobában lévő aktuális kimenő csoportmunkamenet elvetését";

View File

@@ -2974,3 +2974,10 @@
"settings_manage_account_description" = "Kelola akun Anda di %@";
"manage_session_redirect" = "Anda akan dialihkan ke penyedia autentikasi server Anda untuk menyelesaikan proses keluar.";
"manage_session_redirect_error" = "Fungsi saat ini tidak tersedia. Silakan hubungi admin homeserver Anda";
"room_action_report_prompt_reason" = "Alasan melaporkan ruangan ini";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Kecilkan komposer";
"wysiwyg_composer_action_maximise_action" = "Luaskan komposer";
"room_action_report" = "Laporkan ruangan";

View File

@@ -2747,3 +2747,10 @@
"settings_manage_account_description" = "Gestisci il tuo account su %@";
"manage_session_redirect" = "Verrai reindirizzato al fornitore di autenticazione del tuo server per completare la disconnessione.";
"manage_session_redirect_error" = "Funzionalità attualmente non disponibile. Contatta l'amministratore del tuo homeserver";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Rimpicciolisci il compositore";
"wysiwyg_composer_action_maximise_action" = "Espandi il compositore";
"room_action_report" = "Segnala stanza";
"room_action_report_prompt_reason" = "Motivo della segnalazione della stanza";

View File

@@ -1 +1,77 @@
// String for App Store
"store_short_description" = "უსაფრთხო დეცენტრალიზებული ჩატი/ვოიპი";
// Titles
"title_home" = "მთავარი";
"title_favourites" = "ფავორიტები";
"title_people" = "ხალხი";
"title_rooms" = "ოთახები";
"title_groups" = "თემები";
"warning" = "გაფრთხილება";
// Actions
"view" = "ხედი";
"next" = "შემდეგი";
"back" = "უკან";
"continue" = "გაგრძელება";
"create" = "შექმნა";
"start" = "აწყება";
"leave" = "დატოვება";
"remove" = "წაშლა";
"retry" = "გამეორება";
"on" = "ჩართული";
"off" = "გამორთული";
"enable" = "ჩართვა";
"save" = "შენახვა";
"join" = "შეუერთდება";
"decline" = "უარყოფა";
"accept" = "მიღება";
"camera" = "კამერა";
"voice" = "ხმა";
"video" = "ვიდეო";
"active_call" = "აქტიური ზარი";
"later" = "შემდეგ";
"rename" = "გადარქმევა";
"collapse" = "ჩაკეცვა";
"send_to" = "გაგზავნა %@-ს";
"close" = "დახურვა";
"skip" = "გამოტოვება";
"joined" = "შეუერთდა";
"switch" = "გადართვა";
"more" = "მეტი";
"less" = "ნაკლები";
"open" = "გახსნა";
"private" = "ირადი";
"public" = "საჯარო";
"stop" = "შეჩერება";
"new_word" = "ახალი";
"existing" = "არსებული";
"add" = "დამატება";
"ok" = "კარგი";
"error" = "შეცდომა";
"suggest" = "შესთავაზება";
"confirm" = "დადასტურება";
"invite_to" = "მიწვევა %@-ში";
// Activities
"loading" = "ჩატვირთვა";
"sending" = "გაგზავნა";
"callbar_active_and_single_paused" = "1 აქტიური ზარი (%@) · 1 შეჩერებული ზარი";
"callbar_active_and_multiple_paused" = "1 აქტიური ზარი (%@) · %@ შეჩერებული ზარები";
"callbar_only_single_paused" = "შეჩერებული ზარი";
"callbar_return" = "დაბრუნება";
"store_promotional_text" = "კონფიდენციალობის დაცვით ჩატისა და თანამშრომლობის აპლიკაცია, ღია ქსელზე. დეცენტრალიზებული, რათა მართვა გადაგიცემათ. არაა დათამაინინგი, არაა უკანა კარები და არაა მესამე მხარის წვდომა.";
"invite" = "მიწვევა";
"cancel" = "გაუქმება";
"preview" = "წინასწარი ხილვა";
"active_call_details" = "აქტიური ზარი (%@)";
"joining" = "შეერთება";
"done" = "დასრულებული";
"edit" = "რედაქტირება";
"saving" = "შენახვა";
// Call Bar
"callbar_only_single_active" = "შეეხეთ, რომ დაბრუნდეთ ზარში (%@)";
"callbar_only_multiple_paused" = "%@ შეჩერებული ზარები";

View File

@@ -0,0 +1,7 @@
"NSCalendarsUsageDescription" = "Sjå dei planlagde møta dine i appen.";
"NSFaceIDUsageDescription" = "Face ID vert brukt til å få tilgang til appen din.";
// Permissions usage explanations
"NSCameraUsageDescription" = "Kameraet vert brukt til videosamtalar, og til å ta og laste opp bilde og videoar.";
"NSMicrophoneUsageDescription" = "Appen treng tilgang til mikrofonen for samtalar, og for å spele inn video og lydmeldingar.";

View File

@@ -0,0 +1 @@

View File

@@ -9,3 +9,74 @@
"warning" = "Åtvaring";
// String for App Store
"store_short_description" = "Sikker desentralisert chat/IP-telefoni";
"ok" = "OK";
"callbar_only_single_paused" = "Samtale sett på pause";
"cancel" = "Avbryt";
"save" = "Lagra";
"join" = "Ver med";
"decline" = "Avslå";
"accept" = "Godta";
"preview" = "Førehandsvising";
"camera" = "Kamera";
"voice" = "Røyst";
"video" = "Video";
"active_call" = "Aktiv samtale";
"active_call_details" = "Aktiv samtale (%@)";
"joined" = "Vart med";
"later" = "Seinare";
"rename" = "Endre namn";
"collapse" = "Skjul";
"send_to" = "Send til %@";
"sending" = "Sender";
"close" = "Lat att";
"skip" = "Hopp over";
"switch" = "Byt";
"more" = "Meir";
"less" = "Mindre";
"open" = "Opne";
"done" = "Ferdig";
// Call Bar
"callbar_only_single_active" = "Trykk for å gå tilbake til samtalen (%@)";
"callbar_only_multiple_paused" = "%@ samtalar sett på pause";
"callbar_return" = "Gå tilbake";
"callbar_only_single_active_group" = "Trykk for å bli med i konferansesamtalen (%@)";
// Accessibility
"accessibility_checkbox_label" = "avkryssingsboks";
"accessibility_button_label" = "knapp";
"error" = "Feil";
"invite_to" = "Inviter til %@";
// MARK: Onboarding
"onboarding_splash_register_button_title" = "Opprett konto";
"accessibility_selected" = "vald";
"private" = "Privat";
"public" = "Offentleg";
"stop" = "Stopp";
"new_word" = "Ny";
"existing" = "Eksisterande";
"add" = "Legg til";
"suggest" = "Føreslå";
"edit" = "Rediger";
// Activities
"loading" = "Lastar";
"saving" = "Lagrar";
"callbar_active_and_single_paused" = "1 aktiv samtale (%@) · 1 samtale sett i pause";
"callbar_active_and_multiple_paused" = "1 aktiv samtale (%@) · %@ samtalar sett på pause";
"confirm" = "Stadfest";
"onboarding_splash_login_button_title" = "Eg har allereie ein konto";
"authentication_forgot_password_text_field_placeholder" = "E-postadresse";
"next" = "Neste";
"back" = "Tilbake";
"continue" = "Fortset";
"create" = "Lag";
"remove" = "Fjern";
"invite" = "Inviter";
"retry" = "Prøv på nytt";
"on" = "På";
"off" = "Av";
"authentication_login_username" = "Brukarnamn / e-postadresse / telefonnummer";
"onboarding_display_name_placeholder" = "Visingsnamn";
"authentication_login_with_qr" = "Logg inn med QR-kode";

View File

@@ -1226,7 +1226,7 @@
"secrets_recovery_reset_action_part_2" = "Resettar tudo";
"secrets_setup_recovery_passphrase_summary_information" = "Lembre-se de sua Frase de Segurança. Ela pode ser usada para destrancar suas mensagens & dados encriptados.";
"secrets_setup_recovery_passphrase_summary_title" = "Salvar sua Frase de Segurança";
"home_empty_view_information" = "O app de chat seguro tudo-em-um para equipes, amigas(os) e organizações. Toque no botão + abaixo para adicionar pessoas e salas.";
"home_empty_view_information" = "O app de chat seguro tudo-em-um para equipes, amigos e organizações. Toque no botão + abaixo para adicionar pessoas e salas.";
// MARK: - Home
@@ -1244,7 +1244,7 @@
// MARK: - Invite friends
"invite_friends_action" = "Convidar amigas(os) para %@";
"invite_friends_action" = "Convidar amigos para %@";
"pin_protection_settings_change_pin" = "Mudar PIN";
"pin_protection_confirm_pin_to_change" = "Confirme PIN para mudar PIN";
"bug_report_background_mode" = "Continuar em background";
@@ -1348,7 +1348,7 @@
"side_menu_action_feedback" = "Feedback";
"side_menu_action_help" = "Ajuda";
"side_menu_action_settings" = "Ajustes";
"side_menu_action_invite_friends" = "Convidar amigas(os)";
"side_menu_action_invite_friends" = "Convidar amigos";
// Mark: - Side menu
@@ -1632,7 +1632,7 @@
"onboarding_use_case_not_sure_yet" = "Não tem certeza ainda? %@";
"onboarding_use_case_community_messaging" = "Comunidades";
"onboarding_use_case_work_messaging" = "Times";
"onboarding_use_case_personal_messaging" = "Amigas(os) e família";
"onboarding_use_case_personal_messaging" = "Amigos e família";
"onboarding_use_case_message" = "Nós vamos ajudá-la(o) a ficar conectada(o)";
"onboarding_use_case_title" = "Com quem você vai fazer chat mais?";
@@ -2654,3 +2654,20 @@
"user_other_session_security_recommendation_title" = "Outras sessões";
"room_creation_only_one_email_invite" = "Você só pode convidar um e-mail de cada vez";
"accessibility_selected" = "selecionado";
"room_creation_user_not_found_prompt_title" = "Confirmação";
"room_creation_user_not_found_prompt_message" = "Não foi possível encontrar perfis para este ID do Matrix. Quer começar uma conversa mesmo assim?";
"room_creation_user_not_found_prompt_invite_action" = "Começar conversa mesmo assim";
"room_participants_invite_anyway" = "Convidar mesmo assim";
"room_command_part_room_description" = "Sair da sala";
"room_command_kick_user_description" = "Remove o usuário com o ID fornecido desta sala";
"room_command_unban_user_description" = "Desbane o usuário com o ID fornecido";
"room_command_set_user_power_level_description" = "Define o nível de poder de um usuário";
"room_participants_invite_unknown_participant_prompt_to_msg" = "Não foi possível encontrar perfis para este ID do Matrix. Tem certeza que deseja convidar %@ para %@?";
"room_command_ban_user_description" = "Bane o usuário com o ID fornecido";
// Room commands descriptions
"room_command_change_display_name_description" = "Altera o seu nome de exibição";
"room_command_emote_description" = "Exibe ação";
"room_command_join_room_description" = "Entra na sala com o endereço fornecido";
"room_command_invite_user_description" = "Convida o usuário com o ID fornecido para a sala atual";
"authentication_qr_login_failure_device_not_supported" = "Vincular com este dispositivo não é suportado.";

View File

@@ -2970,3 +2970,10 @@
"settings_manage_account_description" = "Spravujte svoj účet na %@";
"manage_session_redirect" = "Budete presmerovaní na poskytovateľa overovania vášho servera, aby ste dokončili odhlásenie.";
"manage_session_redirect_error" = "Funkcia aktuálne nie je dostupná. Obráťte sa na správcu vášho domovského servera";
"room_action_report_prompt_reason" = "Dôvod nahlásenia tejto miestnosti";
"wysiwyg_composer_action_maximise_action" = "Rozšíriť editor";
"room_action_report" = "Nahlásiť miestnosť";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Zmenšiť editor";

View File

@@ -14,7 +14,7 @@
"leave" = "Dilni";
"remove" = "Hiqe";
"invite" = "Ftoje";
"retry" = "Riprovo";
"retry" = "Riprovoni";
"off" = "Off";
"cancel" = "Anuloje";
"save" = "Ruaje";
@@ -2757,3 +2757,10 @@
"settings_manage_account_title" = "Llogari";
"settings_manage_account_action" = "Administroni llogari";
"manage_session_redirect" = "Do të ridrejtoheni te shërbimi i mirëfilltësimit të shërbyesit tuaj, për të plotësuar daljen nga llogaria.";
"room_action_report" = "Raportojeni dhomën";
"room_action_report_prompt_reason" = "Arsye për raportimin e kësaj dhome";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Tkurre hartuesin";
"wysiwyg_composer_action_maximise_action" = "Zgjeroje hartuesin";

View File

@@ -2714,3 +2714,10 @@
"settings_manage_account_description" = "Hantera ditt konto på %@";
"manage_session_redirect" = "Du kommer att omdirigeras till din servers autentiseringsleverantör för att fortsätta utloggning.";
"manage_session_redirect_error" = "Funktion för närvarande otillgänglig. Vänligen kontakta din hemserveradministratör";
"room_action_report" = "Rapportera rum";
"room_action_report_prompt_reason" = "Anledning att rapportera det här rummet";
"wysiwyg_composer_action_maximise_action" = "Expandera redigerare";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Krymp redigerare";

View File

@@ -2972,3 +2972,10 @@
"settings_manage_account_description" = "Керувати обліковим записом у %@";
"manage_session_redirect" = "Вас буде перенаправлено до постачальника автентифікації вашого сервера для завершення виходу.";
"manage_session_redirect_error" = "Функціональність наразі недоступна. Зверніться до адміністратора вашого домашнього сервера";
"wysiwyg_composer_action_maximise_action" = "Розгорнути редактор";
"room_action_report" = "Поскаржитися на кімнату";
"room_action_report_prompt_reason" = "Причина скарги на цю кімнату";
// MARK: - WYSIWYG Composer
"wysiwyg_composer_action_minimise_action" = "Зменшити редактор";

View File

@@ -0,0 +1,57 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
extension Notification.Name {
static let roomSummaryDidRemoveExpiredDataFromStore = Notification.Name(MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore)
}
extension MXRoomSummary {
@objc static let roomSummaryDidRemoveExpiredDataFromStore = "roomSummaryDidRemoveExpiredDataFromStore"
@objc static let roomRetentionStateEventType = "m.room.retention"
@objc static let roomRetentionEventMaxLifetimeKey = "max_lifetime"
@objc static let roomRetentionMaxLifetime = "roomRetentionMaxLifetime"
/// Get the room messages retention period in days
private func roomRetentionPeriodInMillis() -> UInt64 {
if let period = self.others[MXRoomSummary.roomRetentionMaxLifetime] as? UInt64 {
return period
} else {
return Tools.durationInMs(fromDays: 365)
}
}
/// Get the timestamp below which the received messages must be removed from the store, and the display
@objc func minimumTimestamp() -> UInt64 {
let periodInMs = self.roomRetentionPeriodInMillis()
let currentTs = (UInt64)(Date().timeIntervalSince1970 * 1000)
return (currentTs - periodInMs)
}
/// Remove the expired messages from the store.
/// If some data are removed, this operation posts the notification: roomSummaryDidRemoveExpiredDataFromStore.
/// This operation does not commit the potential change. We let the caller trigger the commit when this is the more suitable.
///
/// Provide a boolean telling whether some data have been removed.
@objc func removeExpiredRoomContentsFromStore() -> Bool {
let ret = self.mxSession.store.removeAllMessagesSent(before: self.minimumTimestamp(), inRoom: roomId)
if ret {
NotificationCenter.default.post(name: .roomSummaryDidRemoveExpiredDataFromStore, object: self)
}
return ret
}
}

View File

@@ -25,4 +25,16 @@ extension MXSession {
matrixItemId: userId,
displayName: user?.displayname)
}
/// Clean the storage of a session by removing the expired contents.
@objc func removeExpiredMessages() {
var hasStoreChanged = false
for room in self.rooms {
hasStoreChanged = hasStoreChanged || room.summary.removeExpiredRoomContentsFromStore()
}
if hasStoreChanged {
self.store.commit?()
}
}
}

View File

@@ -5207,6 +5207,14 @@ public class VectorL10n: NSObject {
public static var roomActionReply: String {
return VectorL10n.tr("Vector", "room_action_reply")
}
/// Report room
public static var roomActionReport: String {
return VectorL10n.tr("Vector", "room_action_report")
}
/// Reason for reporting this room
public static var roomActionReportPromptReason: String {
return VectorL10n.tr("Vector", "room_action_report_prompt_reason")
}
/// Send file
public static var roomActionSendFile: String {
return VectorL10n.tr("Vector", "room_action_send_file")
@@ -9567,6 +9575,14 @@ public class VectorL10n: NSObject {
public static var widgetStickerPickerNoStickerpacksAlertAddNow: String {
return VectorL10n.tr("Vector", "widget_sticker_picker_no_stickerpacks_alert_add_now")
}
/// Expand composer
public static var wysiwygComposerActionMaximiseAction: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_action_maximise_action")
}
/// Shrink composer
public static var wysiwygComposerActionMinimiseAction: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_action_minimise_action")
}
/// Apply bold format
public static var wysiwygComposerFormatActionBold: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_bold")

View File

@@ -108,10 +108,6 @@ final class RiotSettings: NSObject {
@UserDefault(key: UserDefaultsKeys.pinRoomsWithUnreadMessagesOnHome, defaultValue: false, storage: defaults)
var pinRoomsWithUnreadMessagesOnHome
/// Indicate to show Not Safe For Work public rooms.
@UserDefault(key: "showNSFWPublicRooms", defaultValue: false, storage: defaults)
var showNSFWPublicRooms
// MARK: User interface
@UserDefault<String?>(key: "userInterfaceTheme", defaultValue: nil, storage: defaults)
@@ -390,9 +386,11 @@ final class RiotSettings: NSObject {
@UserDefault(key: "settingsScreenShowNotificationDecodedContentOption", defaultValue: BWIBuildSettings.shared.settingsScreenShowNotificationDecodedContentOption, storage: defaults)
var settingsScreenShowNotificationDecodedContentOption
// BWI: #6076 removed from nv, need to be checked after merge
@UserDefault(key: "settingsScreenShowNsfwRoomsOption", defaultValue: BWIBuildSettings.shared.settingsScreenShowNsfwRoomsOption, storage: defaults)
var settingsScreenShowNsfwRoomsOption
@UserDefault(key: "settingsSecurityScreenShowSessions", defaultValue: BuildSettings.settingsSecurityScreenShowSessions, storage: defaults)
var settingsSecurityScreenShowSessions

View File

@@ -106,6 +106,9 @@ import AnalyticsEvents
guard let session = session else { return }
useAnalyticsSettings(from: session)
client.updateSuperProperties(.init(appPlatform: .EI,
cryptoSDK: .Rust,
cryptoSDKVersion: session.crypto.version))
}
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
@@ -171,6 +174,13 @@ import AnalyticsEvents
switch result {
case .success(let settings):
self.identify(with: settings)
self.client.updateSuperProperties(
AnalyticsEvent.SuperProperties(
appPlatform: .EI,
cryptoSDK: .Rust,
cryptoSDKVersion: session.crypto.version
)
)
self.service = nil
case .failure:
MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.")
@@ -275,7 +285,9 @@ extension Analytics {
let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName,
ftueUseCaseSelection: ftueUseCase?.analyticsName,
numFavouriteRooms: numFavouriteRooms,
numSpaces: numSpaces)
numSpaces: numSpaces,
recoveryState: nil,
verificationState: nil)
#if POSTHOG
client.updateUserProperties(userProperties)
#endif

View File

@@ -53,4 +53,11 @@ protocol AnalyticsClientProtocol {
/// be a delay when updating user properties as these are cached to be included
/// as part of the next event that gets captured.
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
/// Updates the super properties.
/// Super properties added to all captured events and screen.
/// - Parameter superProperties: The properties event to capture.
func updateSuperProperties(_ event: AnalyticsEvent.SuperProperties)
}

View File

@@ -21,24 +21,34 @@ extension DecryptionFailure {
public func toAnalyticsEvent() -> AnalyticsEvent.Error {
let timeToDecryptMillis: Int = if self.timeToDecrypt != nil {
Int(self.timeToDecrypt! * 1000)
let timeToDecryptMillis: Int = if let ttd = self.timeToDecrypt {
Int(ttd * 1000)
} else {
-1
}
let isHistoricalEvent = if let localAge = self.eventLocalAgeMillis {
localAge < 0
} else { false }
let errorName = if isHistoricalEvent && self.trustOwnIdentityAtTimeOfFailure == false {
AnalyticsEvent.Error.Name.HistoricalMessage
} else {
self.reason.errorName
}
return AnalyticsEvent.Error(
context: self.context,
cryptoModule: .Rust,
cryptoSDK: .Rust,
domain: .E2EE,
eventLocalAgeMillis: nil,
isFederated: nil,
isMatrixDotOrg: nil,
name: self.reason.errorName,
eventLocalAgeMillis: self.eventLocalAgeMillis,
isFederated: self.isFederated,
isMatrixDotOrg: self.isMatrixOrg,
name: errorName,
timeToDecryptMillis: timeToDecryptMillis,
userTrustsOwnIdentity: nil,
wasVisibleToUser: nil
userTrustsOwnIdentity: self.trustOwnIdentityAtTimeOfFailure,
wasVisibleToUser: self.wasVisibleToUser
)
}
}

View File

@@ -52,13 +52,27 @@ import AnalyticsEvents
/// UTDs can be permanent or temporary. If temporary, this field will contain the time it took to decrypt the message in milliseconds. If permanent should be nil
var timeToDecrypt: TimeInterval?
init(failedEventId: String, reason: DecryptionFailureReason, context: String, errorCode: NSInteger, deviceCount: NSInteger, unspecifiedErrorMessage: String?) {
/// Was the current cross-signing identity trusted at the time of decryption
var trustOwnIdentityAtTimeOfFailure: Bool?
var eventLocalAgeMillis: Int?
/// Is the current user on matrix org
var isMatrixOrg: Bool?
/// Are the sender and recipient on the same homeserver
var isFederated: Bool?
/// As for now the ios App only reports UTDs visible to user (error are reported from EventFormatter
var wasVisibleToUser: Bool = true
init(failedEventId: String, reason: DecryptionFailureReason, context: String, errorCode: NSInteger, deviceCount: NSInteger, unspecifiedErrorMessage: String?, ts: TimeInterval) {
self.failedEventId = failedEventId
self.reason = reason
self.context = context
self.errorCode = errorCode
self.deviceCount = deviceCount
self.unspecifiedErrorMessage = unspecifiedErrorMessage
self.ts = ts
}
}

View File

@@ -63,14 +63,14 @@ class DecryptionFailureTracker: NSObject {
selector: #selector(eventDidDecrypt(_:)),
name: .mxEventDidDecrypt,
object: nil)
}
@objc
func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, myUser userId: String) {
func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, mySession: MXSession) {
if reportedFailures[event.eventId] != nil || trackedEvents.contains(event.eventId) {
return
}
guard let userId = mySession.myUserId else { return }
// Filter out "expected" UTDs
// We cannot decrypt messages sent before the user joined the room
@@ -83,6 +83,12 @@ class DecryptionFailureTracker: NSObject {
guard let error = event.decryptionError as? NSError else { return }
let eventOrigin = event.originServerTs
let deviceTimestamp = mySession.crypto.deviceCreationTs
// If negative it's an historical event relative to the current session
let eventRelativeAgeMillis = Int(eventOrigin) - Int(deviceTimestamp)
let isSessionVerified = mySession.crypto.crossSigning.canTrustCrossSigning
var reason = DecryptionFailureReason.unspecified
if error.code == MXDecryptingErrorUnknownInboundSessionIdCode.rawValue {
@@ -93,7 +99,24 @@ class DecryptionFailureTracker: NSObject {
let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription)
reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs())
let failure = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs())
failure.eventLocalAgeMillis = Int(exactly: eventRelativeAgeMillis)
failure.trustOwnIdentityAtTimeOfFailure = isSessionVerified
let myDomain = userId.components(separatedBy: ":").last
failure.isMatrixOrg = myDomain == "matrix.org"
if MXTools.isMatrixUserIdentifier(event.sender) {
let senderDomain = event.sender.components(separatedBy: ":").last
failure.isFederated = senderDomain != nil && senderDomain != myDomain
}
/// XXX for future work, as for now only the event formatter reports UTDs. That means that it's only UTD ~visible to users
failure.wasVisibleToUser = true
reportedFailures[failedEventId] = failure
// Start the ticker if needed. There is no need to have a ticker if no failures are tracked
if checkFailuresTimer == nil {

View File

@@ -16,15 +16,20 @@
#if POSTHOG
import PostHog
extension PHGPostHogConfiguration {
static var standard: PHGPostHogConfiguration? {
// BWI: #6076 need to be checked after merge
// extension PHGPostHogConfiguration {
// static var standard: PHGPostHogConfiguration? {
// let analyticsConfiguration = BWIBuildSettings.shared.analyticsConfiguration
extension PostHogConfig {
static var standard: PostHogConfig? {
let analyticsConfiguration = BWIBuildSettings.shared.analyticsConfiguration
guard analyticsConfiguration.isEnabled else { return nil }
let postHogConfiguration = PHGPostHogConfiguration(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host)
postHogConfiguration.shouldSendDeviceID = false
let postHogConfiguration = PostHogConfig(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host)
// We capture screens manually
postHogConfiguration.captureScreenViews = false
return postHogConfiguration
}
}

View File

@@ -20,31 +20,48 @@ import AnalyticsEvents
/// An analytics client that reports events to a PostHog server.
class PostHogAnalyticsClient: AnalyticsClientProtocol {
private var posthogFactory: PostHogFactory = DefaultPostHogFactory()
init(posthogFactory: PostHogFactory? = nil) {
if let factory = posthogFactory {
self.posthogFactory = factory
}
}
/// The PHGPostHog object used to report events.
private var postHog: PHGPostHog?
private var postHog: PostHogProtocol?
/// Any user properties to be included with the next captured event.
private(set) var pendingUserProperties: AnalyticsEvent.UserProperties?
/// Super Properties are properties associated with events that are set once and then sent with every capture call, be it a $screen, an autocaptured button click, or anything else.
/// It is different from user properties that will be attached to the user and not events.
/// Not persisted for now, should be set on start.
private var superProperties: AnalyticsEvent.SuperProperties?
static let shared = PostHogAnalyticsClient()
var isRunning: Bool { postHog?.enabled ?? false }
var isRunning: Bool {
guard let postHog else { return false }
return !postHog.isOptOut()
}
func start() {
// Only start if analytics have been configured in BuildSettings
guard let configuration = PHGPostHogConfiguration.standard else { return }
guard let configuration = PostHogConfig.standard else { return }
if postHog == nil {
postHog = PHGPostHog(configuration: configuration)
postHog = posthogFactory.createPostHog(config: configuration)
}
postHog?.enable()
postHog?.optIn()
}
func identify(id: String) {
if let userProperties = pendingUserProperties {
// As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
postHog?.identify(id, userProperties: userProperties.properties.compactMapValues { $0 })
pendingUserProperties = nil
} else {
postHog?.identify(id)
@@ -57,10 +74,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
}
func stop() {
postHog?.disable()
postHog?.optOut()
// As of PostHog 1.4.4, setting the client to nil here doesn't release
// it. Keep it around to avoid having multiple instances if the user re-enables
self.postHog = nil
}
func flush() {
@@ -68,11 +84,13 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
}
func capture(_ event: AnalyticsEventProtocol) {
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
postHog?.capture(event.eventName, properties: attachSuperProperties(to: event.properties), userProperties: pendingUserProperties?.properties.compactMapValues { $0 })
// Pending user properties have been added
self.pendingUserProperties = nil
}
func screen(_ event: AnalyticsScreenProtocol) {
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
postHog?.screen(event.screenName.rawValue, properties: attachSuperProperties(to: event.properties))
}
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
@@ -85,25 +103,35 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
self.pendingUserProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter,
ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection,
numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms,
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces)
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces,
// Not yet supported
recoveryState: nil, verificationState: nil)
}
// MARK: - Private
func updateSuperProperties(_ updatedProperties: AnalyticsEvent.SuperProperties) {
self.superProperties = AnalyticsEvent.SuperProperties(
appPlatform: updatedProperties.appPlatform ?? superProperties?.appPlatform,
cryptoSDK: updatedProperties.cryptoSDK ?? superProperties?.cryptoSDK,
cryptoSDKVersion: updatedProperties.cryptoSDKVersion ?? superProperties?.cryptoSDKVersion
)
}
/// Given a dictionary containing properties from an event, this method will return those properties
/// with any pending user properties included under the `$set` key.
/// - Parameter properties: A dictionary of properties from an event.
/// - Returns: The `properties` dictionary with any user properties included.
private func attachUserProperties(to properties: [String: Any]) -> [String: Any] {
guard isRunning, let userProperties = pendingUserProperties else { return properties }
/// Attach super properties to events.
/// If the property is already set on the event, the already set value will be kept.
private func attachSuperProperties(to properties: [String: Any]) -> [String: Any] {
guard isRunning, let superProperties else { return properties }
var properties = properties
// As user properties overwrite old ones via $set, compactMap the dictionary to avoid resetting any missing properties
properties["$set"] = userProperties.properties.compactMapValues { $0 }
pendingUserProperties = nil
superProperties.properties.forEach { (key: String, value: Any) in
if properties[key] == nil {
properties[key] = value
}
}
return properties
}
}
extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol {

View File

@@ -0,0 +1,53 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import PostHog
protocol PostHogProtocol {
func optIn()
func optOut()
func reset()
func flush()
func capture(_ event: String, properties: [String: Any]?, userProperties: [String: Any]?)
func screen(_ screenTitle: String, properties: [String: Any]?)
func isFeatureEnabled(_ feature: String) -> Bool
func identify(_ distinctId: String)
func identify(_ distinctId: String, userProperties: [String: Any]?)
func isOptOut() -> Bool
}
protocol PostHogFactory {
func createPostHog(config: PostHogConfig) -> PostHogProtocol
}
class DefaultPostHogFactory: PostHogFactory {
func createPostHog(config: PostHogConfig) -> PostHogProtocol {
PostHogSDK.shared.setup(config)
return PostHogSDK.shared
}
}
extension PostHogSDK: PostHogProtocol { }

View File

@@ -1908,6 +1908,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[self registerNewRequestNotificationForSession:mxSession];
[self.pushNotificationService checkPushKitPushersInSession:mxSession];
// Clean the storage by removing expired data
[mxSession removeExpiredMessages];
}
else if (mxSession.state == MXSessionStateRunning)
{

View File

@@ -512,7 +512,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
if (!_publicRoomsDirectoryDataSource)
{
_publicRoomsDirectoryDataSource = [[PublicRoomsDirectoryDataSource alloc] initWithMatrixSession:mxSession];
_publicRoomsDirectoryDataSource.showNSFWRooms = RiotSettings.shared.showNSFWPublicRooms;
_publicRoomsDirectoryDataSource.delegate = self;
}

View File

@@ -147,8 +147,6 @@
// Reset searches
[recentsDataSource searchWithPatterns:nil];
// TODO: Notify RiotSettings.shared.showNSFWPublicRooms change for iPad as viewWillAppear may not be called
recentsDataSource.publicRoomsDirectoryDataSource.showNSFWRooms = RiotSettings.shared.showNSFWPublicRooms;
[self updateSearch];

View File

@@ -45,11 +45,6 @@
*/
@property (nonatomic) BOOL includeAllNetworks;
/**
Flag to indicate to show Not Safe For Work rooms in the public room list.
*/
@property (nonatomic) BOOL showNSFWRooms;
/**
List public rooms from a third party protocol.
Default is nil.

View File

@@ -46,6 +46,8 @@ static NSString *const kNSFWKeyword = @"nsfw";
NSString *nextBatch;
}
@property (nonatomic, strong) NSRegularExpression *forbiddenTermsRegex;
@end
@implementation PublicRoomsDirectoryDataSource
@@ -57,6 +59,15 @@ static NSString *const kNSFWKeyword = @"nsfw";
{
rooms = [NSMutableArray array];
_paginationLimit = 20;
NSString *path = [[NSBundle mainBundle] pathForResource:@"forbidden_terms" ofType:@"txt"];
NSString *fileContents = [NSString stringWithContentsOfFile:path encoding: NSUTF8StringEncoding error:nil];
NSArray *forbiddenTerms = [fileContents componentsSeparatedByCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet];
NSString *pattern = [NSString stringWithFormat:@"\\b(%@)\\b", [forbiddenTerms componentsJoinedByString:@"|"]];
pattern = [pattern stringByAppendingString:@"|(\\b18\\+)"]; // Special case "18+"
_forbiddenTermsRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
}
return self;
}
@@ -155,16 +166,6 @@ static NSString *const kNSFWKeyword = @"nsfw";
}
}
- (void)setShowNSFWRooms:(BOOL)showNSFWRooms
{
if (showNSFWRooms != _showNSFWRooms)
{
_showNSFWRooms = showNSFWRooms;
[self resetPagination];
}
}
- (NSUInteger)roomsCount
{
return rooms.count;
@@ -254,14 +255,7 @@ static NSString *const kNSFWKeyword = @"nsfw";
NSArray<MXPublicRoom*> *publicRooms;
if (self.showNSFWRooms)
{
publicRooms = publicRoomsResponse.chunk;
}
else
{
publicRooms = [self filterPublicRooms:publicRoomsResponse.chunk containingKeyword:kNSFWKeyword];
}
publicRooms = [self filterPublicRooms:publicRoomsResponse.chunk];
[self->rooms addObjectsFromArray:publicRooms];
self->nextBatch = publicRoomsResponse.nextBatch;
@@ -338,15 +332,23 @@ static NSString *const kNSFWKeyword = @"nsfw";
}
}
- (NSArray<MXPublicRoom*>*)filterPublicRooms:(NSArray<MXPublicRoom*>*)publicRooms containingKeyword:(NSString*)keyword
- (NSArray<MXPublicRoom*>*)filterPublicRooms:(NSArray<MXPublicRoom*>*)publicRooms
{
NSMutableArray *filteredRooms = [NSMutableArray new];
for (MXPublicRoom *publicRoom in publicRooms)
{
if (NO == [[publicRoom.name lowercaseString] containsString:keyword]
&& NO == [[publicRoom.topic lowercaseString] containsString:keyword])
{
BOOL shouldAllow = YES;
if (publicRoom.name != nil) {
shouldAllow &= [self.forbiddenTermsRegex numberOfMatchesInString:publicRoom.name options:0 range:NSMakeRange(0, publicRoom.name.length)] == 0;
}
if (publicRoom.topic != nil) {
shouldAllow &= [self.forbiddenTermsRegex numberOfMatchesInString:publicRoom.topic options:0 range:NSMakeRange(0, publicRoom.topic.length)] == 0;
}
if (shouldAllow) {
[filteredRooms addObject:publicRoom];
}
}

View File

@@ -40,6 +40,10 @@ enum RendezvousFlow: String {
case SETUP_ADDITIONAL_DEVICE_V2 = "org.matrix.msc3906.setup.additional_device.v2"
}
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO: define an end-of-life date for this implementation.
/// Allows communication through a secure channel. Based on MSC3886 and MSC3903
@MainActor
class RendezvousService {

View File

@@ -107,6 +107,8 @@
@property (nonatomic) BOOL customAnimationsEnabled;
@property (nonatomic) BOOL isLoadingVideo;
@end
@implementation MXKAttachmentsViewController
@@ -976,8 +978,10 @@
navigationBarDisplayTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(hideNavigationBar) userInfo:self repeats:NO];
}
}
else
else if (!self.isLoadingVideo)
{
self.isLoadingVideo = YES;
MXKPieChartView *pieChartView = [[MXKPieChartView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
pieChartView.progress = 0;
pieChartView.progressColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.25];
@@ -1027,6 +1031,7 @@
[selectedCell.moviePlayer.player play];
[pieChartView removeFromSuperview];
self.isLoadingVideo = NO;
[self hideNavigationBar];
}
@@ -1042,6 +1047,7 @@
MXLogDebug(@"[MXKAttachmentsVC] video download failed");
[pieChartView removeFromSuperview];
self.isLoadingVideo = NO;
// Display the navigation bar so that the user can leave this screen
self.navigationBarContainer.hidden = NO;

View File

@@ -34,6 +34,9 @@ const CGFloat kTypingCellHeight = 24;
{
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
// The listener to the room retention changes.
id retentionListener;
}
// Observe key verification request changes
@@ -168,6 +171,31 @@ const CGFloat kTypingCellHeight = 24;
self.eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages;
}
- (void)setDelegate:(id<MXKDataSourceDelegate>)delegate
{
[self unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications];
[self removeRoomRetentionEventListener];
if (delegate && self.isLive)
{
if (self.room)
{
// Remove the potential expired messages from the store
if ([self.room.summary removeExpiredRoomContentsFromStore])
{
[self.mxSession.store commit];
}
[self addRoomRetentionEventListener];
}
// Observe room history flush (expired content data)
[self registerRoomSummaryDidRemoveExpiredDataFromStoreNotifications];
[self roomSummaryDidRemoveExpiredDataFromStore];
}
[super setDelegate:delegate];
}
- (void)destroy
{
if (kThemeServiceDidChangeThemeNotificationObserver)
@@ -198,6 +226,9 @@ const CGFloat kTypingCellHeight = 24;
[self.mxSession.aggregations.beaconAggregations removeListener:self.beaconInfoSummaryDeletionListener];
}
[self unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications];
[self removeRoomRetentionEventListener];
[super destroy];
}
@@ -1284,4 +1315,79 @@ const CGFloat kTypingCellHeight = 24;
}
}
#pragma mark - roomSummaryDidRemoveExpiredDataFromStore notifications
- (void)registerRoomSummaryDidRemoveExpiredDataFromStoreNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(roomSummaryDidRemoveExpiredDataFromStore:) name:MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore object:nil];
}
- (void)unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore object:nil];
}
- (void)roomSummaryDidRemoveExpiredDataFromStore:(NSNotification*)notification
{
MXRoomSummary *roomSummary = notification.object;
if (self.mxSession == roomSummary.mxSession && [self.roomId isEqualToString:roomSummary.roomId])
{
[self roomSummaryDidRemoveExpiredDataFromStore];
}
}
- (void)roomSummaryDidRemoveExpiredDataFromStore
{
// Check whether the first cell data refers to an expired event (this may be a state event
MXEvent *firstMessageEvent;
for (id<MXKRoomBubbleCellDataStoring> cellData in bubbles)
{
for (MXEvent *event in cellData.events)
{
if (!event.isState) {
firstMessageEvent = event;
break;
}
}
if (firstMessageEvent)
{
break;
}
}
if (firstMessageEvent && firstMessageEvent.originServerTs < self.room.summary.minimumTimestamp)
{
[self reload];
}
}
#pragma mark - room retention event listener
- (void)addRoomRetentionEventListener
{
// Register a listener to handle the room retention in live timelines
retentionListener = [self.timeline listenToEventsOfTypes:@[MXRoomSummary.roomRetentionStateEventType] onEvent:^(MXEvent *redactionEvent, MXTimelineDirection direction, MXRoomState *roomState) {
// Consider only live events
if (direction == MXTimelineDirectionForwards)
{
// Remove the potential expired messages from the store
if ([self.room.summary removeExpiredRoomContentsFromStore])
{
[self.mxSession.store commit];
}
}
}];
}
- (void)removeRoomRetentionEventListener
{
if (retentionListener)
{
[self.timeline removeListener:retentionListener];
retentionListener = nil;
}
}
@end

View File

@@ -237,6 +237,9 @@ extension RoomInfoCoordinator: RoomInfoListCoordinatorDelegate {
self.delegate?.roomInfoCoordinatorDidLeaveRoom(self)
}
func roomInfoListCoordinatorDidRequestReportRoom(_ coordinator: RoomInfoListCoordinatorType) {
self.delegate?.roomInfoCoordinatorDidRequestReportRoom(self)
}
}
extension RoomInfoCoordinator: RoomParticipantsViewControllerDelegate {

View File

@@ -25,6 +25,7 @@ import MatrixSDK
func roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String)
func roomInfoCoordinatorBridgePresenter(_ coordinator: RoomInfoCoordinatorBridgePresenter, viewEventInTimeline event: MXEvent)
func roomInfoCoordinatorBridgePresenterDidRequestReportRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
}
/// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller.
@@ -131,9 +132,14 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate {
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) {
self.delegate?.roomInfoCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId)
}
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) {
self.delegate?.roomInfoCoordinatorBridgePresenter(self, viewEventInTimeline: event)
}
func roomInfoCoordinatorDidRequestReportRoom(_ coordinator: RoomInfoCoordinatorType) {
self.delegate?.roomInfoCoordinatorBridgePresenterDidRequestReportRoom(self)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate

View File

@@ -25,6 +25,7 @@ protocol RoomInfoCoordinatorDelegate: AnyObject {
func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent)
func roomInfoCoordinatorDidRequestReportRoom(_ coordinator: RoomInfoCoordinatorType)
}
/// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.

View File

@@ -75,4 +75,7 @@ extension RoomInfoListCoordinator: RoomInfoListViewModelCoordinatorDelegate {
self.delegate?.roomInfoListCoordinatorDidLeaveRoom(self)
}
func roomInfoListViewModelDidRequestReportRoom(_ viewModel: RoomInfoListViewModelType) {
self.delegate?.roomInfoListCoordinatorDidRequestReportRoom(self)
}
}

View File

@@ -22,6 +22,7 @@ protocol RoomInfoListCoordinatorDelegate: AnyObject {
func roomInfoListCoordinator(_ coordinator: RoomInfoListCoordinatorType, wantsToNavigateTo target: RoomInfoListTarget)
func roomInfoListCoordinatorDidCancel(_ coordinator: RoomInfoListCoordinatorType)
func roomInfoListCoordinatorDidLeaveRoom(_ coordinator: RoomInfoListCoordinatorType)
func roomInfoListCoordinatorDidRequestReportRoom(_ coordinator: RoomInfoListCoordinatorType)
}
/// `RoomInfoListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.

View File

@@ -52,4 +52,5 @@ enum RoomInfoListViewAction {
case leave
case cancel
case permalink
case report
}

View File

@@ -271,8 +271,16 @@ final class RoomInfoListViewController: UIViewController {
rows: [rowLeave],
footer: nil)
let rowReport = Row(type: .destructive, icon: Asset.Images.error.image, text: VectorL10n.roomEventActionReport, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .report)
}
let sectionReport = Section(header: nil,
rows: [rowReport],
footer: nil)
tmpSections.append(sectionSettings)
tmpSections.append(sectionLeave)
tmpSections.append(sectionReport)
sections = tmpSections
}

View File

@@ -118,6 +118,8 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType {
self.coordinatorDelegate?.roomInfoListViewModelDidCancel(self)
case .permalink:
self.createPermalink()
case .report:
self.coordinatorDelegate?.roomInfoListViewModelDidRequestReportRoom(self)
}
}

View File

@@ -27,6 +27,7 @@ protocol RoomInfoListViewModelCoordinatorDelegate: AnyObject {
func roomInfoListViewModelDidCancel(_ viewModel: RoomInfoListViewModelType)
func roomInfoListViewModelDidLeaveRoom(_ viewModel: RoomInfoListViewModelType)
func roomInfoListViewModel(_ viewModel: RoomInfoListViewModelType, wantsToNavigateTo target: RoomInfoListTarget)
func roomInfoListViewModelDidRequestReportRoom(_ viewModel: RoomInfoListViewModelType)
}
/// Protocol describing the view model used by `RoomInfoListViewController`

View File

@@ -5495,6 +5495,50 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
- (void)handleReportRoom
{
// Prompt user to enter a description of the problem content.
UIAlertController *reportReasonAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomActionReportPromptReason]
message:nil
preferredStyle:UIAlertControllerStyleAlert];
[reportReasonAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.placeholder = nil;
textField.keyboardType = UIKeyboardTypeDefault;
}];
MXWeakify(self);
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
NSString *text = [self->currentAlert textFields].firstObject.text;
self->currentAlert = nil;
[self startActivityIndicator];
[self.roomDataSource.mxSession.matrixRestClient reportRoom:self.roomDataSource.roomId reason:text success:^{
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
MXLogDebug(@"[RoomVC] Report room (%@) failed", self.roomDataSource.roomId);
//Alert user
[self showError:error];
}];
}]];
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
MXStrongifyAndReturnIfNil(self);
self->currentAlert = nil;
}]];
[self presentViewController:reportReasonAlert animated:YES completion:nil];
self->currentAlert = reportReasonAlert;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
@@ -5740,6 +5784,10 @@ static CGSize kThreadListBarButtonItemImageSize;
{
[self presentDeclineOptionsFromView:tappedView];
}
else if (tappedView == previewHeader.reportButton)
{
[self handleReportRoom];
}
}
- (void)presentDeclineOptionsFromView:(UIView *)view
@@ -8242,6 +8290,11 @@ static CGSize kThreadListBarButtonItemImageSize;
[self reloadRoomWihtEventId:event.eventId threadId:event.threadId forceUpdateRoomMarker:NO];
}
- (void)roomInfoCoordinatorBridgePresenterDidRequestReportRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[self handleReportRoom];
}
-(void)reloadRoomWihtEventId:(NSString *)eventId
threadId:(NSString *)threadId
forceUpdateRoomMarker:(BOOL)forceUpdateRoomMarker

View File

@@ -32,6 +32,7 @@
@property (weak, nonatomic) IBOutlet UIView *buttonsContainer;
@property (weak, nonatomic) IBOutlet UIButton *leftButton;
@property (weak, nonatomic) IBOutlet UIButton *rightButton;
@property (weak, nonatomic) IBOutlet UIButton *reportButton;
@property (weak, nonatomic) IBOutlet UILabel *subNoticeLabel;
@property (weak, nonatomic) IBOutlet UIView *bottomBorderView;

View File

@@ -51,12 +51,22 @@
[self.rightButton setTitle:[BWIL10n join] forState:UIControlStateNormal];
[self.rightButton setTitle:[BWIL10n join] forState:UIControlStateHighlighted];
[self.reportButton setTitle:[VectorL10n roomActionReport] forState:UIControlStateNormal];
[self.reportButton setTitle:[VectorL10n roomActionReport] forState:UIControlStateHighlighted];
tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(reportTapGesture:)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[self.rightButton addGestureRecognizer:tap];
self.rightButton.userInteractionEnabled = YES;
tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(reportTapGesture:)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[self.reportButton addGestureRecognizer:tap];
self.reportButton.userInteractionEnabled = YES;
}
-(void)customizeViewRendering
@@ -96,6 +106,7 @@
{
[self.rightButton setTitleColor: ThemeService.shared.theme.backgroundColor forState:UIControlStateNormal];
}
[self.reportButton setTitleColor:ThemeService.shared.theme.warningColor forState:UIControlStateNormal];
}
- (void)refreshDisplay

View File

@@ -1,25 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="Cpa-Xt-cwB" customClass="PreviewRoomTitleView">
<rect key="frame" x="0.0" y="0.0" width="600" height="355"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="386"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="y5s-FK-My4">
<rect key="frame" x="0.0" y="0.0" width="600" height="215"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
<accessibility key="accessibilityConfiguration" identifier="MainHeaderBackground"/>
<constraints>
<constraint firstAttribute="height" constant="215" id="YtX-K2-uD7"/>
@@ -38,7 +34,7 @@
</constraints>
</view>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Room Name" textAlignment="center" adjustsFontSizeToFit="NO" minimumFontSize="14" translatesAutoresizingMaskIntoConstraints="NO" id="6uH-I3-RQg">
<rect key="frame" x="249" y="126" width="103" height="22"/>
<rect key="frame" x="249.5" y="126" width="101" height="23"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="DisplayNameTextField"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="18"/>
@@ -48,9 +44,9 @@
</connections>
</textField>
<textView opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" editable="NO" text="Room Topic" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="Tk0-pA-9a0">
<rect key="frame" x="31" y="153" width="538" height="17"/>
<rect key="frame" x="31" y="154" width="538" height="16"/>
<accessibility key="accessibilityConfiguration" identifier="RoomTopic"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<color key="textColor" systemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textView>
@@ -58,11 +54,11 @@
<rect key="frame" x="262" y="184" width="76" height="17"/>
<accessibility key="accessibilityConfiguration" identifier="RoomMembers"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="details_icon" translatesAutoresizingMaskIntoConstraints="NO" id="S3Y-wJ-HOe">
<rect key="frame" x="345" y="187" width="6" height="12"/>
<rect key="frame" x="345" y="186.5" width="6" height="12"/>
<accessibility key="accessibilityConfiguration" identifier="RoomMembersDetailsIcon"/>
<constraints>
<constraint firstAttribute="width" constant="6" id="XTx-6p-2wB"/>
@@ -94,17 +90,17 @@
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gIX-nY-f6M">
<rect key="frame" x="280" y="239" width="41" height="20"/>
<rect key="frame" x="279.5" y="239" width="41" height="20"/>
<accessibility key="accessibilityConfiguration" identifier="PreviewLabel"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WmW-5h-jL1" userLabel="buttonsContainer">
<rect key="frame" x="60" y="276" width="480" height="30"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="osd-JP-2az" userLabel="Left Button">
<rect key="frame" x="0.0" y="0.0" width="230" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="osd-JP-2az" userLabel="Left Button">
<rect key="frame" x="0.0" y="0.0" width="230.5" height="30"/>
<color key="backgroundColor" red="0.6886889638" green="1" blue="0.74383144840000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="LeftButton"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
@@ -112,8 +108,8 @@
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="461-vO-hLZ" userLabel="Right Button">
<rect key="frame" x="250" y="0.0" width="230" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="461-vO-hLZ" userLabel="Right Button">
<rect key="frame" x="249.5" y="0.0" width="230.5" height="30"/>
<color key="backgroundColor" red="0.6886889638" green="1" blue="0.74383144840000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="RightButton"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
@@ -135,15 +131,23 @@
<constraint firstItem="osd-JP-2az" firstAttribute="height" secondItem="WmW-5h-jL1" secondAttribute="height" id="zHG-qI-kwU"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Giu-1w-duc">
<rect key="frame" x="239.5" y="310" width="121" height="31"/>
<color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Report content">
<fontDescription key="titleFontDescription" type="system" pointSize="14"/>
</buttonConfiguration>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IwA-0X-IYb">
<rect key="frame" x="281" y="323" width="38" height="18"/>
<rect key="frame" x="281" y="344" width="38" height="18"/>
<accessibility key="accessibilityConfiguration" identifier="SubNoticeLabel"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BCO-DT-F0C">
<rect key="frame" x="0.0" y="354" width="600" height="1"/>
<rect key="frame" x="0.0" y="375" width="600" height="1"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="BottomBorderView"/>
<constraints>
@@ -154,15 +158,17 @@
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="RoomTitle"/>
<constraints>
<constraint firstItem="IwA-0X-IYb" firstAttribute="top" secondItem="WmW-5h-jL1" secondAttribute="bottom" constant="17" id="0ft-2Y-gk3"/>
<constraint firstAttribute="trailing" secondItem="WmW-5h-jL1" secondAttribute="trailing" constant="60" id="1b7-i7-NOb"/>
<constraint firstAttribute="trailing" secondItem="BkF-x3-7fX" secondAttribute="trailing" id="33r-D5-3uy"/>
<constraint firstItem="Giu-1w-duc" firstAttribute="top" secondItem="WmW-5h-jL1" secondAttribute="bottom" constant="4" id="4rk-St-IBy"/>
<constraint firstItem="gIX-nY-f6M" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Cpa-Xt-cwB" secondAttribute="leading" constant="31" id="7Kn-g9-Ya6"/>
<constraint firstItem="WmW-5h-jL1" firstAttribute="leading" secondItem="Cpa-Xt-cwB" secondAttribute="leading" constant="60" id="8dy-U2-a3G"/>
<constraint firstItem="BCO-DT-F0C" firstAttribute="top" secondItem="IwA-0X-IYb" secondAttribute="bottom" constant="13" id="G2S-aH-ycH"/>
<constraint firstItem="BCO-DT-F0C" firstAttribute="leading" secondItem="Cpa-Xt-cwB" secondAttribute="leading" id="H07-HA-2Px"/>
<constraint firstItem="y5s-FK-My4" firstAttribute="leading" secondItem="Cpa-Xt-cwB" secondAttribute="leading" id="IU0-R1-jNV"/>
<constraint firstAttribute="trailing" secondItem="BCO-DT-F0C" secondAttribute="trailing" id="Izl-ms-lGJ"/>
<constraint firstItem="Giu-1w-duc" firstAttribute="centerX" secondItem="Cpa-Xt-cwB" secondAttribute="centerX" id="KCY-vw-EHD"/>
<constraint firstItem="IwA-0X-IYb" firstAttribute="top" secondItem="Giu-1w-duc" secondAttribute="bottom" constant="3" id="Kfj-HY-Vdh"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="gIX-nY-f6M" secondAttribute="trailing" constant="31" id="Ljb-d3-KKM"/>
<constraint firstItem="BkF-x3-7fX" firstAttribute="leading" secondItem="Cpa-Xt-cwB" secondAttribute="leading" id="Uvb-cK-dQf"/>
<constraint firstItem="BkF-x3-7fX" firstAttribute="top" secondItem="Cpa-Xt-cwB" secondAttribute="top" id="Zru-9A-ifB"/>
@@ -187,6 +193,7 @@
<outlet property="mainHeaderBackgroundHeightConstraint" destination="YtX-K2-uD7" id="47y-aL-t94"/>
<outlet property="mainHeaderContainer" destination="BkF-x3-7fX" id="1fp-xz-ETJ"/>
<outlet property="previewLabel" destination="gIX-nY-f6M" id="Bxi-6M-rTb"/>
<outlet property="reportButton" destination="Giu-1w-duc" id="gZe-uF-CoI"/>
<outlet property="rightButton" destination="461-vO-hLZ" id="u8R-Rg-WAv"/>
<outlet property="roomAvatar" destination="4yt-FK-V2Z" id="epr-4f-7cE"/>
<outlet property="roomMembers" destination="ou0-3Z-weL" id="lRs-fz-QXc"/>
@@ -194,9 +201,16 @@
<outlet property="roomTopic" destination="Tk0-pA-9a0" id="iAe-bC-f6X"/>
<outlet property="subNoticeLabel" destination="IwA-0X-IYb" id="aUb-Pt-V44"/>
</connections>
<point key="canvasLocation" x="-27.199999999999999" y="-1.7991004497751126"/>
</view>
</objects>
<resources>
<image name="details_icon" width="31" height="31"/>
<image name="details_icon" width="6" height="11"/>
<systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="groupTableViewBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -60,6 +60,16 @@
{
iconURL = [NSString stringWithFormat:@"%@%@", kMXContentUriScheme, [iconURL substringFromIndex:range.location + range.length]];
}
// Check also if we are using the authenticated endpoint
else
{
mxMediaPrefix = [NSString stringWithFormat:@"/%@/download/", kMXAuthenticatedContentPrefixPath];
range = [iconURL rangeOfString:mxMediaPrefix];
if (range.location != NSNotFound)
{
iconURL = [NSString stringWithFormat:@"%@%@", kMXContentUriScheme, [iconURL substringFromIndex:range.location + range.length]];
}
}
[self.iconImageView setImageURI:iconURL
withType:nil
andImageOrientation:UIImageOrientationUp

View File

@@ -196,8 +196,7 @@ typedef NS_ENUM(NSUInteger, PRESENCE)
typedef NS_ENUM(NSUInteger, ADVANCED)
{
ADVANCED_SHOW_NSFW_ROOMS_INDEX = 0,
ADVANCED_CRASH_REPORT_INDEX,
ADVANCED_CRASH_REPORT_INDEX = 0,
ADVANCED_ENABLE_RAGESHAKE_INDEX,
ADVANCED_MARK_ALL_AS_READ_INDEX,
ADVANCED_CLEAR_CACHE_INDEX,
@@ -725,6 +724,7 @@ SSOAuthenticationPresenterDelegate>
Section *sectionAdvanced = [Section sectionWithTag:SECTION_TAG_ADVANCED];
sectionAdvanced.headerTitle = [VectorL10n settingsAdvanced];
// BWI: #6076 check after merge
if (RiotSettings.shared.settingsScreenShowNsfwRoomsOption)
{
[sectionAdvanced addRowWithTag:ADVANCED_SHOW_NSFW_ROOMS_INDEX];

View File

@@ -520,8 +520,12 @@ extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate {
self.remove(childCoordinator: coordinator)
}
}
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) {
}
func roomInfoCoordinatorDidRequestReportRoom(_ coordinator: RoomInfoCoordinatorType) {
}
}

View File

@@ -0,0 +1,68 @@
anal
bbw
bdsm
beast
bestiality
blowjob
bondage
boobs
clit
cock
cuck
cum
cunt
daddy
dick
dildo
erotic
exhibitionism
faggot
femboy
fisting
flogging
fmf
foursome
futa
gangbang
gore
h3ntai
handjob
hentai
incest
jizz
kink
loli
m4f
masturbate
masturbation
mfm
milf
moresome
naked
neet
nsfw
nude
nudity
orgy
pedo
pegging
penis
petplay
porn
pussy
rape
rimming
sadism
sadomasochism
sexy
shota
spank
squirt
strap-on
threesome
vagina
vibrator
voyeur
watersports
xxx
zoo

View File

@@ -344,7 +344,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
// Track e2e failures
dispatch_async(dispatch_get_main_queue(), ^{
[[BWIAnalyticsHelper shared] getRoomDeviceCountWithRoom:[self->mxSession roomWithRoomId:roomState.roomId] completion:^(NSInteger deviceCount) {
[[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState myUser:self->mxSession.myUser.userId roomDeviceCount:deviceCount];
[[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState mySession:self->mxSession];
}];
});
}
@@ -665,7 +665,14 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray<MXEvent *> *)stateEvents roomState:(MXRoomState *)roomState
{
BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState];
MXEvent* lastRoomRetentionEvent = [self roomRetentionEventFromStateEvents:stateEvents];
if (lastRoomRetentionEvent)
{
summary.others[MXRoomSummary.roomRetentionMaxLifetime] = lastRoomRetentionEvent.content[MXRoomSummary.roomRetentionEventMaxLifetimeKey];
updated = YES;
}
// Customisation for EMS Functional Members in direct rooms
if (BuildSettings.supportFunctionalMembers && summary.room.isDirect)
{
@@ -678,7 +685,7 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
// room name which we'll do twice more in updateRoomSummary:withServerRoomSummary:roomState: anyway.
//
// So return YES and let that happen there.
return YES;
updated = YES;
}
}
@@ -834,6 +841,12 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
return [stateEvents filteredArrayUsingPredicate:functionalMembersPredicate].lastObject;
}
- (MXEvent *)roomRetentionEventFromStateEvents:(NSArray<MXEvent *> *)stateEvents
{
NSPredicate *functionalMembersPredicate = [NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringRoomRetention];
return [stateEvents filteredArrayUsingPredicate:functionalMembersPredicate].lastObject;
}
#pragma mark - Timestamp formatting
- (NSString*)dateStringFromDate:(NSDate *)date withTime:(BOOL)time

View File

@@ -51,4 +51,11 @@
+ (NSURL*)fixURLWithSeveralHashKeys:(NSURL*)url;
+ (NSData*)mediaConvertMaxImageData:(NSData*) imageData withUTI:(MXKUTI *)uti;
#pragma mark - Time utilities
/**
* Convert a number of days to a duration in ms.
*/
+ (uint64_t)durationInMsFromDays:(uint)days;
@end

View File

@@ -196,4 +196,11 @@
return convertedData;
}
#pragma mark - Time utilities
+ (uint64_t)durationInMsFromDays:(uint)days
{
return days * (uint64_t)(86400000);
}
@end

View File

@@ -41,6 +41,7 @@ targets:
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
- package: PostHog
configFiles:
Debug: Debug.xcconfig

View File

@@ -36,6 +36,7 @@ targets:
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
- package: PostHog
configFiles:
Debug: Debug.xcconfig

View File

@@ -23,6 +23,9 @@ import ZXingObjC
// MARK: - QRLoginService
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO: define an end-of-life date for this implementation.
class QRLoginService: NSObject, QRLoginServiceProtocol {
private let client: AuthenticationRestClient
private let sessionCreator: SessionCreatorProtocol

View File

@@ -82,6 +82,10 @@ struct Composer: View {
wysiwygViewModel.maximised ? "minimiseButton" : "maximiseButton"
}
private var toggleButtonAccessibilityLabel: String {
wysiwygViewModel.maximised ? VectorL10n.wysiwygComposerActionMinimiseAction : VectorL10n.wysiwygComposerActionMaximiseAction
}
private var toggleButtonImageName: String {
wysiwygViewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name
}
@@ -171,6 +175,7 @@ struct Composer: View {
.frame(width: 16, height: 16)
}
.accessibilityIdentifier(toggleButtonAcccessibilityIdentifier)
.accessibilityLabel(toggleButtonAccessibilityLabel)
.padding(.leading, 12)
.padding(.trailing, 4)
}

View File

@@ -78,7 +78,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")
// When updating the user properties
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5, recoveryState: nil, verificationState: nil))
// Then the properties should be cached
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
@@ -90,7 +90,7 @@ class AnalyticsTests: XCTestCase {
func testMergingUserProperties() {
// Given a client with a cached use case user properties
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
@@ -98,7 +98,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")
// When updating the number of spaces
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5, recoveryState: nil, verificationState: nil))
// Then the new properties should be updated and the existing properties should remain unchanged
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
@@ -107,7 +107,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
// When updating the number of spaces
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
// Then the new properties should be updated and the existing properties should remain unchanged
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
@@ -120,14 +120,15 @@ class AnalyticsTests: XCTestCase {
func testSendingUserProperties() {
// Given a client with user properties set
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
client.start()
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
// When sending an event (tests run under Debug configuration so this is sent to the development instance)
client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home))
let event = AnalyticsEvent.Signup(authenticationType: .Other)
client.capture(event)
// Then the properties should be cleared
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
@@ -136,7 +137,7 @@ class AnalyticsTests: XCTestCase {
func testSendingUserPropertiesWithIdentify() {
// Given a client with user properties set
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
client.start()
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")

View File

@@ -41,10 +41,23 @@ class DecryptionFailureTrackerTests: XCTestCase {
}
let timeShifter = TimeShifter()
var fakeCrypto: FakeCrypto!
var fakeSession: FakeSession!
var fakeCrossSigning: FakeCrossSigning!
override func setUp() {
super.setUp()
self.fakeCrypto = FakeCrypto()
self.fakeCrossSigning = FakeCrossSigning()
self.fakeCrypto.crossSigning = self.fakeCrossSigning
self.fakeSession = FakeSession(mockCrypto: self.fakeCrypto)
}
func test_grace_period() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -61,7 +74,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
timeShifter.timestamp = TimeInterval(2)
@@ -82,7 +95,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_report_ratcheted_key_utd() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -99,7 +113,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Pass the max period
timeShifter.timestamp = TimeInterval(70)
@@ -111,7 +125,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_report_unspecified_error() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -128,7 +143,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Pass the max period
timeShifter.timestamp = TimeInterval(70)
@@ -142,7 +157,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_do_not_double_report() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -160,7 +176,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Pass the max period
timeShifter.timestamp = TimeInterval(70)
@@ -171,7 +187,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
// Try to report again the same event
testDelegate.reportedFailure = nil
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Pass the grace period
timeShifter.timestamp = TimeInterval(10)
@@ -183,7 +199,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_ignore_not_member() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -203,7 +220,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
fakeMembers.mockMembers[myUser] = MXMembership.ban
fakeRoomState.mockMembers = fakeMembers
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Pass the grace period
timeShifter.timestamp = TimeInterval(5)
@@ -217,7 +234,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_notification_center() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -235,7 +253,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Shift time below GRACE_PERIOD
timeShifter.timestamp = TimeInterval(2)
@@ -257,7 +275,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_should_report_late_decrypt() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -275,7 +294,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Simulate succesful decryption after grace period but before max wait
timeShifter.timestamp = TimeInterval(20)
@@ -301,7 +320,8 @@ class DecryptionFailureTrackerTests: XCTestCase {
func test_should_report_permanent_decryption_error() {
let myUser = "test@example.com";
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
@@ -319,7 +339,7 @@ class DecryptionFailureTrackerTests: XCTestCase {
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser);
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Simulate succesful decryption after max wait
timeShifter.timestamp = TimeInterval(70)
@@ -337,5 +357,246 @@ class DecryptionFailureTrackerTests: XCTestCase {
XCTAssertEqual(analyticsError.timeToDecryptMillis, -1)
}
func test_should_report_trust_status_at_decryption_time() {
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
let testDelegate = AnalyticsDelegate();
decryptionFailureTracker.delegate = testDelegate;
timeShifter.timestamp = TimeInterval(0)
let fakeEvent = FakeEvent(id: "$0000");
fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
// set session as not yet verified
fakeCrossSigning.canTrustCrossSigning = false
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// set verified now
fakeCrossSigning.canTrustCrossSigning = true
// Simulate succesful decryption after max wait
timeShifter.timestamp = TimeInterval(70)
decryptionFailureTracker.checkFailures();
// Event should have been reported as a late decrypt
XCTAssertEqual(testDelegate.reportedFailure?.trustOwnIdentityAtTimeOfFailure, false);
// Assert that it's converted to -1 for reporting
let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError.userTrustsOwnIdentity, false)
// Report a new error now that session is verified
let fakeEvent2 = FakeEvent(id: "$0001");
fakeEvent2.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent2, withRoomState: fakeRoomState, mySession: fakeSession);
// Simulate permanent UTD
timeShifter.timestamp = TimeInterval(140)
decryptionFailureTracker.checkFailures();
XCTAssertEqual(testDelegate.reportedFailure?.failedEventId, "$0001");
XCTAssertEqual(testDelegate.reportedFailure?.trustOwnIdentityAtTimeOfFailure, true);
let analyticsError2 = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError2.userTrustsOwnIdentity, true)
}
func test_should_report_event_age() {
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let sessionCreationTimeMillis = format.date(from: "2024-03-09T10:00:00Z")!.timeIntervalSince1970 * 1000
let now = format.date(from: "2024-03-09T10:02:00Z")!.timeIntervalSince1970
// 5mn after session was created
let postCreationMessageTs = UInt64(format.date(from: "2024-03-09T10:05:00Z")!.timeIntervalSince1970 * 1000)
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
let testDelegate = AnalyticsDelegate();
decryptionFailureTracker.delegate = testDelegate;
timeShifter.timestamp = now
let fakeEvent = FakeEvent(id: "$0000");
fakeEvent.mockOrigineServerTs = postCreationMessageTs;
fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
fakeCrypto.deviceCreationTs = UInt64(sessionCreationTimeMillis)
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// set verified now
fakeCrossSigning.canTrustCrossSigning = true
// Simulate permanent UTD
timeShifter.timestamp = now + TimeInterval(70)
decryptionFailureTracker.checkFailures();
XCTAssertEqual(testDelegate.reportedFailure?.eventLocalAgeMillis, 5 * 60 * 1000);
let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError.eventLocalAgeMillis, 5 * 60 * 1000)
}
func test_should_report_expected_utds() {
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let sessionCreationTimeMillis = format.date(from: "2024-03-09T10:00:00Z")!.timeIntervalSince1970 * 1000
let now = format.date(from: "2024-03-09T10:02:00Z")!.timeIntervalSince1970
// 1 day before session was created
let historicalMessageTs = UInt64(format.date(from: "2024-03-08T10:00:00Z")!.timeIntervalSince1970 * 1000)
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
let testDelegate = AnalyticsDelegate();
decryptionFailureTracker.delegate = testDelegate;
timeShifter.timestamp = now
let fakeEvent = FakeEvent(id: "$0000");
fakeEvent.mockOrigineServerTs = historicalMessageTs;
fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
fakeCrypto.deviceCreationTs = UInt64(sessionCreationTimeMillis)
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
fakeCrossSigning.canTrustCrossSigning = false
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// set verified now
fakeCrossSigning.canTrustCrossSigning = true
// Simulate permanent UTD
timeShifter.timestamp = now + TimeInterval(70)
decryptionFailureTracker.checkFailures();
// Event should have been reported as a late decrypt
XCTAssertEqual(testDelegate.reportedFailure?.eventLocalAgeMillis, -24 * 60 * 60 * 1000);
let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError.name, .HistoricalMessage)
}
func test_should_report_is_matrix_org_and_is_federated() {
let myUser = "@test:example.com";
fakeSession.mockUserId = myUser;
let decryptionFailureTracker = DecryptionFailureTracker();
decryptionFailureTracker.timeProvider = timeShifter;
let testDelegate = AnalyticsDelegate();
decryptionFailureTracker.delegate = testDelegate;
timeShifter.timestamp = TimeInterval(0)
let fakeEvent = FakeEvent(id: "$0000");
fakeEvent.sender = "@bob:example.com"
fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
// set session as not yet verified
fakeCrossSigning.canTrustCrossSigning = false
let fakeRoomState = FakeRoomState();
fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession);
// Simulate succesful decryption after max wait
timeShifter.timestamp = TimeInterval(70)
decryptionFailureTracker.checkFailures();
XCTAssertEqual(testDelegate.reportedFailure?.isMatrixOrg, false);
XCTAssertEqual(testDelegate.reportedFailure?.isFederated, false);
let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError.isMatrixDotOrg, false)
XCTAssertEqual(analyticsError.isFederated, false)
// Report a new error now that session is verified
let fakeEvent2 = FakeEvent(id: "$0001");
fakeEvent2.sender = "@bob:example.com"
fakeEvent2.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue))
fakeSession.mockUserId = "@test:matrix.org";
fakeRoomState.mockMembers = FakeRoomMembers(joined: [fakeSession.mockUserId])
decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent2, withRoomState: fakeRoomState, mySession: fakeSession);
// Simulate permanent UTD
timeShifter.timestamp = TimeInterval(140)
decryptionFailureTracker.checkFailures();
XCTAssertEqual(testDelegate.reportedFailure?.failedEventId, "$0001");
XCTAssertEqual(testDelegate.reportedFailure?.isMatrixOrg, true);
XCTAssertEqual(testDelegate.reportedFailure?.isFederated, true);
let analyticsError2 = testDelegate.reportedFailure!.toAnalyticsEvent()
XCTAssertEqual(analyticsError2.isMatrixDotOrg, true)
XCTAssertEqual(analyticsError2.isFederated, true)
}
}

View File

@@ -1,4 +1,4 @@
//
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +15,8 @@
//
import Foundation
import PostHog
@testable import Element
class FakeEvent: MXEvent {
@@ -22,6 +24,7 @@ class FakeEvent: MXEvent {
var mockEventId: String;
var mockSender: String!;
var mockDecryptionError: Error?
var mockOrigineServerTs: UInt64 = 0
init(id: String) {
mockEventId = id
@@ -31,7 +34,7 @@ class FakeEvent: MXEvent {
required init?(coder: NSCoder) {
fatalError()
}
override var sender: String! {
get { return mockSender }
set { mockSender = newValue }
@@ -47,6 +50,12 @@ class FakeEvent: MXEvent {
set { mockDecryptionError = newValue }
}
override var originServerTs: UInt64 {
get { return mockOrigineServerTs }
set { mockOrigineServerTs = newValue }
}
}
@@ -80,7 +89,7 @@ class FakeRoomMember: MXRoomMember {
get { return mockUserId }
set { mockUserId = newValue }
}
}
@@ -107,3 +116,364 @@ class FakeRoomMembers: MXRoomMembers {
}
}
class FakeSession: MXSession {
var mockCrypto: MXCrypto? = FakeCrypto()
var mockUserId: String = "@alice:localhost"
init(mockCrypto: MXCrypto? = nil) {
self.mockCrypto = mockCrypto
super.init()
}
override var crypto: MXCrypto? {
get {
return mockCrypto
}
set {
// nothing
}
}
override var myUserId: String! {
get {
return mockUserId
}
set {
mockUserId = newValue
}
}
}
class FakeCrypto: NSObject, MXCrypto {
var version: String = ""
var deviceCurve25519Key: String?
var deviceEd25519Key: String?
var deviceCreationTs: UInt64 = 0
var backup: MXKeyBackup?
var keyVerificationManager: MXKeyVerificationManager = FakeKeyVerificationManager()
var crossSigning: MXCrossSigning = FakeCrossSigning()
var recoveryService: MXRecoveryService! = nil
var dehydrationService: MatrixSDK.DehydrationService! = nil
override init() {
super.init()
}
func start(_ onComplete: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func close(_ deleteStore: Bool) {
}
func isRoomEncrypted(_ roomId: String) -> Bool {
return true
}
func encryptEventContent(_ eventContent: [AnyHashable : Any], withType eventType: String, in room: MXRoom, success: (([AnyHashable : Any], String) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? {
return nil
}
func decryptEvents(_ events: [MXEvent], inTimeline timeline: String?, onComplete: (([MXEventDecryptionResult]) -> Void)? = nil) {
}
func ensureEncryption(inRoom roomId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? {
return nil
}
func eventDeviceInfo(_ event: MXEvent) -> MXDeviceInfo? {
return nil
}
func discardOutboundGroupSessionForRoom(withRoomId roomId: String, onComplete: (() -> Void)? = nil) {
}
func handle(_ syncResponse: MXSyncResponse, onComplete: @escaping () -> Void) {
}
func setDeviceVerification(_ verificationStatus: MXDeviceVerification, forDevice deviceId: String, ofUser userId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func setUserVerification(_ verificationStatus: Bool, forUser userId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func trustLevel(forUser userId: String) -> MXUserTrustLevel {
return MXUserTrustLevel.init(crossSigningVerified: true, locallyVerified: true)
}
func deviceTrustLevel(forDevice deviceId: String, ofUser userId: String) -> MXDeviceTrustLevel? {
return nil
}
func trustLevelSummary(forUserIds userIds: [String], forceDownload: Bool, success: ((MXUsersTrustLevelSummary?) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func downloadKeys(_ userIds: [String], forceDownload: Bool, success: ((MXUsersDevicesMap<MXDeviceInfo>?, [String : MXCrossSigningInfo]?) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? {
return nil
}
func devices(forUser userId: String) -> [String : MXDeviceInfo] {
return [:];
}
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? {
return nil
}
func exportRoomKeys(withPassword password: String, success: ((Data) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func importRoomKeys(_ keyFile: Data, withPassword password: String, success: ((UInt, UInt) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func reRequestRoomKey(for event: MXEvent) {
}
var globalBlacklistUnverifiedDevices: Bool = false
func isBlacklistUnverifiedDevices(inRoom roomId: String) -> Bool {
return false
}
func setBlacklistUnverifiedDevicesInRoom(_ roomId: String, blacklist: Bool) {
}
func invalidateCache(_ done: @escaping () -> Void) {
}
}
class FakeCrossSigning: NSObject, MXCrossSigning {
override init() {
super.init()
}
var state: MXCrossSigningState = MXCrossSigningState.trustCrossSigning
var myUserCrossSigningKeys: MXCrossSigningInfo? = nil
var canTrustCrossSigning: Bool = true
var canCrossSign: Bool = true
var hasAllPrivateKeys: Bool = true
func refreshState(success: ((Bool) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) {
}
func setup(withPassword password: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func setup(withAuthParams authParams: [AnyHashable : Any], success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func crossSignDevice(withDeviceId deviceId: String, userId: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func signUser(withUserId userId: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func crossSigningKeys(forUser userId: String) -> MXCrossSigningInfo? {
return nil
}
}
class FakeKeyVerificationManager: NSObject, MXKeyVerificationManager {
override init() {
super.init()
}
func requestVerificationByToDevice(withUserId userId: String, deviceIds: [String]?, methods: [String], success: @escaping (MXKeyVerificationRequest) -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func requestVerificationByDM(withUserId userId: String, roomId: String?, fallbackText: String, methods: [String], success: @escaping (MXKeyVerificationRequest) -> Void, failure: @escaping (Swift.Error) -> Void) {
}
var pendingRequests: [MXKeyVerificationRequest] = []
func beginKeyVerification(from request: MXKeyVerificationRequest, method: String, success: @escaping (MXKeyVerificationTransaction) -> Void, failure: @escaping (Swift.Error) -> Void) {
}
func transactions(_ complete: @escaping ([MXKeyVerificationTransaction]) -> Void) {
}
func keyVerification(fromKeyVerificationEvent event: MXEvent, roomId: String, success: @escaping (MXKeyVerification) -> Void, failure: @escaping (Swift.Error) -> Void) -> MXHTTPOperation? {
return nil
}
func qrCodeTransaction(withTransactionId transactionId: String) -> MXQRCodeTransaction? {
return nil
}
func removeQRCodeTransaction(withTransactionId transactionId: String) {
}
}
class MockPostHog: PostHogProtocol {
private var enabled = false
func optIn() {
enabled = true
}
func optOut() {
enabled = false
}
func reset() { }
func flush() {
}
var capturePropertiesUserPropertiesUnderlyingCallsCount = 0
var capturePropertiesUserPropertiesCallsCount: Int {
get {
if Thread.isMainThread {
return capturePropertiesUserPropertiesUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = capturePropertiesUserPropertiesUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
capturePropertiesUserPropertiesUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
capturePropertiesUserPropertiesUnderlyingCallsCount = newValue
}
}
}
}
var capturePropertiesUserPropertiesCalled: Bool {
return capturePropertiesUserPropertiesCallsCount > 0
}
var capturePropertiesUserPropertiesReceivedArguments: (event: String, properties: [String: Any]?, userProperties: [String: Any]?)?
var capturePropertiesUserPropertiesReceivedInvocations: [(event: String, properties: [String: Any]?, userProperties: [String: Any]?)] = []
var capturePropertiesUserPropertiesClosure: ((String, [String: Any]?, [String: Any]?) -> Void)?
func capture(_ event: String, properties: [String: Any]?, userProperties: [String: Any]?) {
if !enabled { return }
capturePropertiesUserPropertiesCallsCount += 1
capturePropertiesUserPropertiesReceivedArguments = (event: event, properties: properties, userProperties: userProperties)
capturePropertiesUserPropertiesReceivedInvocations.append((event: event, properties: properties, userProperties: userProperties))
capturePropertiesUserPropertiesClosure?(event, properties, userProperties)
}
var screenPropertiesUnderlyingCallsCount = 0
var screenPropertiesCallsCount: Int {
get {
if Thread.isMainThread {
return screenPropertiesUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = screenPropertiesUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
screenPropertiesUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
screenPropertiesUnderlyingCallsCount = newValue
}
}
}
}
var screenPropertiesCalled: Bool {
return screenPropertiesCallsCount > 0
}
var screenPropertiesReceivedArguments: (screenTitle: String, properties: [String: Any]?)?
var screenPropertiesReceivedInvocations: [(screenTitle: String, properties: [String: Any]?)] = []
var screenPropertiesClosure: ((String, [String: Any]?) -> Void)?
func screen(_ screenTitle: String, properties: [String: Any]?) {
if !enabled { return }
screenPropertiesCallsCount += 1
screenPropertiesReceivedArguments = (screenTitle: screenTitle, properties: properties)
screenPropertiesReceivedInvocations.append((screenTitle: screenTitle, properties: properties))
screenPropertiesClosure?(screenTitle, properties)
}
func isFeatureEnabled(_ feature: String) -> Bool {
return true
}
func identify(_ distinctId: String) {
}
func identify(_ distinctId: String, userProperties: [String : Any]?) {
}
func isOptOut() -> Bool {
!enabled
}
}
class MockPostHogFactory: PostHogFactory {
var mock: PostHogProtocol!
init(mock: PostHogProtocol) {
self.mock = mock
}
func createPostHog(config: PostHogConfig) -> PostHogProtocol {
mock
}
}

View File

@@ -0,0 +1,113 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import Element
import AnalyticsEvents
class PostHogAnalyticsClientTests: XCTestCase {
private var posthogMock: MockPostHog!
override func setUp() {
posthogMock = MockPostHog()
}
func testSuperPropertiesAddedToAllCaptured() {
let analyticsClient = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
analyticsClient.start()
let superProperties = AnalyticsEvent.SuperProperties(appPlatform: .EI, cryptoSDK: .Rust, cryptoSDKVersion: "0.0")
analyticsClient.updateSuperProperties(superProperties)
// It should be the same for any event
let someEvent = AnalyticsEvent.CallEnded(durationMs: 0, isVideo: false, numParticipants: 1, placed: true)
analyticsClient.capture(someEvent)
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
// All the super properties should have been added
XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
XCTAssertEqual(capturedEvent?.properties?["appPlatform"] as? String, AnalyticsEvent.SuperProperties.AppPlatform.EI.rawValue)
XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "0.0")
// Other properties should be there
XCTAssertEqual(capturedEvent?.properties?["isVideo"] as? Bool, false)
// Should also work for screens
analyticsClient.screen(AnalyticsEvent.MobileScreen.init(durationMs: 0, screenName: .Home))
let capturedScreen = posthogMock.screenPropertiesReceivedArguments
XCTAssertEqual(capturedScreen?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
XCTAssertEqual(capturedScreen?.properties?["appPlatform"] as? String, AnalyticsEvent.SuperProperties.AppPlatform.EI.rawValue)
XCTAssertEqual(capturedScreen?.properties?["cryptoSDKVersion"] as? String, "0.0")
XCTAssertEqual(capturedScreen?.screenTitle, AnalyticsEvent.MobileScreen.ScreenName.Home.rawValue)
}
func testSuperPropertiesCanBeUdpated() {
let analyticsClient = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
analyticsClient.start()
let superProperties = AnalyticsEvent.SuperProperties(appPlatform: .EI, cryptoSDK: .Rust, cryptoSDKVersion: "0.0")
analyticsClient.updateSuperProperties(superProperties)
// It should be the same for any event
let someEvent = AnalyticsEvent.CallEnded(durationMs: 0, isVideo: false, numParticipants: 1, placed: true)
analyticsClient.capture(someEvent)
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
//
XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "0.0")
analyticsClient.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EI, cryptoSDK: .Rust, cryptoSDKVersion: "1.0"))
analyticsClient.capture(someEvent)
let secondCapturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
XCTAssertEqual(secondCapturedEvent?.properties?["cryptoSDKVersion"] as? String, "1.0")
}
func testSuperPropertiesDontOverrideEventProperties() {
let analyticsClient = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
analyticsClient.start()
// Super property for cryptoSDK is rust
let superProperties = AnalyticsEvent.SuperProperties(appPlatform: nil, cryptoSDK: .Rust, cryptoSDKVersion: nil)
analyticsClient.updateSuperProperties(superProperties)
// This event as a similar named property `cryptoSDK` with Legacy value
let someEvent = AnalyticsEvent.Error(context: nil, cryptoModule: nil, cryptoSDK: .Legacy, domain: .E2EE, eventLocalAgeMillis: nil, isFederated: nil, isMatrixDotOrg: nil, name: .OlmKeysNotSentError, timeToDecryptMillis: nil, userTrustsOwnIdentity: nil, wasVisibleToUser: nil)
analyticsClient.capture(someEvent)
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.Error.CryptoSDK.Legacy.rawValue)
}
}

View File

@@ -42,7 +42,7 @@ cp -R ../../../.. /tmp/$REPO_NAME
mv /tmp/$REPO_NAME .
else
echo "Git clone $REPO_URL with branch/tag $TAG..."
git clone $REPO_URL --depth=1 --branch $TAG
git clone --recursive $REPO_URL --depth=1 --branch $TAG
fi
cd $REPO_NAME
@@ -55,11 +55,6 @@ bundle update
# Update fastlane plugins
bundle exec fastlane update_plugins
# Use appropriated dependencies according to the current branch
if [ "$LOCAL_SOURCE" != true ]; then
bundle exec fastlane point_dependencies_to_same_feature
fi
# Build
bundle exec fastlane app_store build_number:$BUILD_NUMBER git_tag:$TAG

1
matrix-ios-sdk Submodule

Submodule matrix-ios-sdk added at 1c4b3f5c32

View File

@@ -49,7 +49,7 @@ include:
packages:
AnalyticsEvents:
url: https://github.com/matrix-org/matrix-analytics-events
exactVersion: 0.15.0
exactVersion: 0.23.1
Mapbox:
url: https://github.com/maplibre/maplibre-gl-native-distribution
minVersion: 5.12.2
@@ -66,8 +66,8 @@ packages:
minVersion: 3.5.0
maxVersion: 3.5.0
WysiwygComposer:
url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift
version: 2.29.0
url: https://github.com/matrix-org/matrix-rich-text-editor-swift
version: 2.37.3
DeviceKit:
url: https://github.com/devicekit/DeviceKit
majorVersion: 4.7.0