Merge commit '2e9bb7067e9f1596f29b986729822184145906db' into feature/4068_merge_foss_1_9_15
# Conflicts: # Config/AppVersion.xcconfig # Podfile.lock # Riot/Assets/de.lproj/Localizable.strings # Riot/Modules/Room/RoomViewController.m # RiotNSE/NotificationService.swift # fastlane/Fastfile
@@ -1,3 +1,43 @@
|
||||
## Changes in 1.9.15 (2023-01-10)
|
||||
|
||||
✨ Features
|
||||
|
||||
- Threads: Load the thread list using server-side sorting and pagination ([#6059](https://github.com/vector-im/element-ios/issues/6059))
|
||||
- Rich Text Composer: added link creation/editing feature. ([#7159](https://github.com/vector-im/element-ios/issues/7159))
|
||||
- Rich Text Composer: added inline code formatting feature. ([#7177](https://github.com/vector-im/element-ios/issues/7177))
|
||||
- Voice Broadcast: allow to react on Voice Broadcast. ([#7179](https://github.com/vector-im/element-ios/issues/7179))
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Labs: VoiceBroadcast: Add backward and forward buttons for playback ([#7146](https://github.com/vector-im/element-ios/pull/7146))
|
||||
- Update the room description in the rooms list in case of live broadcast (incoming or outgoing) ([#7160](https://github.com/vector-im/element-ios/pull/7160))
|
||||
- Labs: VoiceBroadcast: Link the live icon color to the recording state ([#7163](https://github.com/vector-im/element-ios/pull/7163))
|
||||
- Add old device data from user's account data events. ([#7164](https://github.com/vector-im/element-ios/pull/7164))
|
||||
- Labs: VoiceBroadcast: Replace the player timeline ([#7165](https://github.com/vector-im/element-ios/pull/7165))
|
||||
- Labs: VoiceBroadcast: Update Voice Broadcast recorder cell by adjusting some padding values ([#7175](https://github.com/vector-im/element-ios/pull/7175))
|
||||
- Labs: VoiceBroadcast: Update live badge layout for recorder and player cells ([#7178](https://github.com/vector-im/element-ios/pull/7178))
|
||||
- Updates on the UI/UX to conform the device manager to the design. ([#7180](https://github.com/vector-im/element-ios/pull/7180))
|
||||
- Labs: VoiceBroadcast: Handle potential crash whereas a voice broadcast is in progress ([#7188](https://github.com/vector-im/element-ios/pull/7188))
|
||||
- Polls: show decryption errors in timeline during aggregations. ([#7206](https://github.com/vector-im/element-ios/pull/7206))
|
||||
- Device Manager: change fallback display name for sessions. ([#7214](https://github.com/vector-im/element-ios/pull/7214))
|
||||
- Ignore the voice broadcast chunks at the notifications level ([#7230](https://github.com/vector-im/element-ios/pull/7230))
|
||||
- Polls: render the poll ended event in the timeline. ([#7231](https://github.com/vector-im/element-ios/pull/7231))
|
||||
- Upgrade MatrixSDK version ([v0.24.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.7)).
|
||||
- Updated fastlane script to use Xcode v 14.2. ([#7182](https://github.com/vector-im/element-ios/issues/7182))
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- Labs: Crash on new voice broadcast if the room has avatar ([#7173](https://github.com/vector-im/element-ios/pull/7173))
|
||||
- Fix hidden live location timeline tiles after text messages ([#7220](https://github.com/vector-im/element-ios/pull/7220))
|
||||
- Fix an issue preventing temporary audio files to be deleted. ([#7244](https://github.com/vector-im/element-ios/pull/7244))
|
||||
- App Layout: wrap Space names to 1 line only in the bottom sheet ([#6579](https://github.com/vector-im/element-ios/issues/6579))
|
||||
- Timeline: fixed navigation back from replies. ([#7003](https://github.com/vector-im/element-ios/issues/7003))
|
||||
- Timeline: fixed an issue where formatted links appeared in black. ([#7109](https://github.com/vector-im/element-ios/issues/7109))
|
||||
- Voice Broadcast: Pause voice broadcast listening on new voice broadcast recording ([#7192](https://github.com/vector-im/element-ios/issues/7192))
|
||||
- Direct Message: fixed a crash when a new DM room is created ([#7232](https://github.com/vector-im/element-ios/issues/7232))
|
||||
- Voice Broadcast: Prevent sending voice message during a voice broadcast recording ([#7235](https://github.com/vector-im/element-ios/issues/7235))
|
||||
|
||||
|
||||
## Changes in 1.9.14 (2022-12-13)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 2.2.0
|
||||
MARKETING_VERSION = 2.3.0
|
||||
CURRENT_PROJECT_VERSION = 20220714163152
|
||||
|
||||
@@ -16,7 +16,7 @@ use_frameworks!
|
||||
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixSDKVersion = '= 0.24.6'
|
||||
$matrixSDKVersion = '= 0.24.7'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
||||
@@ -38,7 +38,6 @@ PODS:
|
||||
- DTFoundation/Core
|
||||
- DTFoundation/UIKit (1.7.18):
|
||||
- DTFoundation/Core
|
||||
- DTTJailbreakDetection (0.4.0)
|
||||
- FLEX (4.5.0)
|
||||
- FlowCommoniOS (1.12.2)
|
||||
- GBDeviceInfo (7.1.0):
|
||||
@@ -56,12 +55,9 @@ PODS:
|
||||
- LoggerAPI (1.9.200):
|
||||
- Logging (~> 1.1)
|
||||
- Logging (1.4.0)
|
||||
- MatomoTracker (7.4.1):
|
||||
- MatomoTracker/Core (= 7.4.1)
|
||||
- MatomoTracker/Core (7.4.1)
|
||||
- MatrixSDK (0.24.6):
|
||||
- MatrixSDK/Core (= 0.24.6)
|
||||
- MatrixSDK/Core (0.24.6):
|
||||
- MatrixSDK (0.24.7):
|
||||
- MatrixSDK/Core (= 0.24.7)
|
||||
- MatrixSDK/Core (0.24.7):
|
||||
- AFNetworking (~> 4.0.0)
|
||||
- GZIP (~> 1.3.0)
|
||||
- libbase58 (~> 0.1.4)
|
||||
@@ -69,12 +65,12 @@ PODS:
|
||||
- OLMKit (~> 3.2.5)
|
||||
- Realm (= 10.27.0)
|
||||
- SwiftyBeaver (= 1.9.5)
|
||||
- MatrixSDK/CryptoSDK (0.24.6):
|
||||
- MatrixSDKCrypto (= 0.1.6)
|
||||
- MatrixSDK/JingleCallStack (0.24.6):
|
||||
- MatrixSDK/CryptoSDK (0.24.7):
|
||||
- MatrixSDKCrypto (= 0.1.7)
|
||||
- MatrixSDK/JingleCallStack (0.24.7):
|
||||
- JitsiMeetSDK (= 5.0.2)
|
||||
- MatrixSDK/Core
|
||||
- MatrixSDKCrypto (0.1.6)
|
||||
- MatrixSDKCrypto (0.1.7)
|
||||
- OLMKit (3.2.12):
|
||||
- OLMKit/olmc (= 3.2.12)
|
||||
- OLMKit/olmcpp (= 3.2.12)
|
||||
@@ -119,7 +115,6 @@ DEPENDENCIES:
|
||||
- DSBottomSheet (~> 0.3)
|
||||
- DSWaveformImage (~> 6.1.1)
|
||||
- DTCoreText (~> 1.6.25)
|
||||
- DTTJailbreakDetection (~> 0.4.0)
|
||||
- FLEX (~> 4.5.0)
|
||||
- FlowCommoniOS (~> 1.12.0)
|
||||
- GBDeviceInfo (~> 7.1.0)
|
||||
@@ -127,9 +122,8 @@ DEPENDENCIES:
|
||||
- KeychainAccess (~> 4.2.2)
|
||||
- KTCenterFlowLayout (~> 1.3.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatomoTracker (~> 7.4.1)
|
||||
- MatrixSDK (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.24.6_bwi_beta`)
|
||||
- MatrixSDK/JingleCallStack (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.24.6_bwi_beta`)
|
||||
- MatrixSDK (= 0.24.7)
|
||||
- MatrixSDK/JingleCallStack (= 0.24.7)
|
||||
- OLMKit
|
||||
- PostHog (~> 1.4.4)
|
||||
- ReadMoreTextView (~> 3.0.1)
|
||||
@@ -158,7 +152,6 @@ SPEC REPOS:
|
||||
- DSWaveformImage
|
||||
- DTCoreText
|
||||
- DTFoundation
|
||||
- DTTJailbreakDetection
|
||||
- FLEX
|
||||
- FlowCommoniOS
|
||||
- GBDeviceInfo
|
||||
@@ -172,7 +165,7 @@ SPEC REPOS:
|
||||
- libPhoneNumber-iOS
|
||||
- LoggerAPI
|
||||
- Logging
|
||||
- MatomoTracker
|
||||
- MatrixSDK
|
||||
- MatrixSDKCrypto
|
||||
- OLMKit
|
||||
- PostHog
|
||||
@@ -197,17 +190,11 @@ EXTERNAL SOURCES:
|
||||
AnalyticsEvents:
|
||||
:branch: release/swift
|
||||
:git: https://github.com/matrix-org/matrix-analytics-events.git
|
||||
MatrixSDK:
|
||||
:git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk
|
||||
:tag: v0.24.6_bwi_beta
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
AnalyticsEvents:
|
||||
:commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f
|
||||
:git: https://github.com/matrix-org/matrix-analytics-events.git
|
||||
MatrixSDK:
|
||||
:git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk
|
||||
:tag: v0.24.6_bwi_beta
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
|
||||
@@ -220,7 +207,6 @@ SPEC CHECKSUMS:
|
||||
DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce
|
||||
DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce
|
||||
DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536
|
||||
DTTJailbreakDetection: 5e356c5badc17995f65a83ed9483f787a0057b71
|
||||
FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b
|
||||
FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2
|
||||
GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376
|
||||
@@ -234,9 +220,8 @@ SPEC CHECKSUMS:
|
||||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb
|
||||
MatrixSDK: 2bd63890d709683741452de2f215cfcda840fe64
|
||||
MatrixSDKCrypto: b9e9bced53510f063bb203ccbec919f08d8f2641
|
||||
MatrixSDK: 895929fad10b7ec9aa96d557403b44c5e3522211
|
||||
MatrixSDKCrypto: 2bd9ca41b2c644839f4e680a64897d56b3f95392
|
||||
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
|
||||
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
|
||||
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
|
||||
@@ -256,6 +241,6 @@ SPEC CHECKSUMS:
|
||||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 2046a454e97a09b21239c91eeeabb97af1089baf
|
||||
PODFILE CHECKSUM: 56782e2abd382278b3c5b23824ca74994fd0a97e
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
|
||||
"state" : {
|
||||
"revision" : "38ad28bedbe63b3587126158245659b6c989ec2c"
|
||||
"revision" : "534ee5bae5e8de69ed398937b5edb7b5f21551d2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_backward_30s.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4763 23.1499C9.82784 23.1499 7.55443 22.2702 5.65607 20.5107C3.7577 18.7513 2.70665 16.5751 2.50292 13.9822C2.4844 13.7599 2.55386 13.5655 2.71128 13.3988C2.86871 13.2321 3.0678 13.1488 3.30857 13.1488C3.53082 13.1488 3.72066 13.2275 3.87808 13.3849C4.03551 13.5423 4.13274 13.7414 4.16978 13.9822C4.37351 16.1121 5.2625 17.8947 6.83676 19.33C8.41101 20.7654 10.2909 21.4831 12.4763 21.4831C14.7914 21.4831 16.7592 20.6728 18.3798 19.0522C20.0003 17.4317 20.8106 15.4638 20.8106 13.1488C20.8106 10.8337 20.0235 8.86587 18.4492 7.24531C16.875 5.62475 14.9303 4.81447 12.6152 4.81447H12.004L13.4486 6.25908C13.6153 6.42577 13.6987 6.62024 13.6987 6.84248C13.6987 7.06473 13.6153 7.2592 13.4486 7.42588C13.2819 7.59257 13.0875 7.67591 12.8652 7.67591C12.643 7.67591 12.4485 7.59257 12.2818 7.42588L9.36483 4.50888C9.27222 4.41628 9.2074 4.32368 9.17036 4.23107C9.13332 4.13847 9.1148 4.0366 9.1148 3.92548C9.1148 3.81436 9.13332 3.71249 9.17036 3.61989C9.2074 3.52729 9.27222 3.43468 9.36483 3.34208L12.3096 0.397297C12.4578 0.249131 12.643 0.175049 12.8652 0.175049C13.0875 0.175049 13.2819 0.249131 13.4486 0.397297C13.5968 0.563982 13.6709 0.758449 13.6709 0.980697C13.6709 1.20294 13.5968 1.38815 13.4486 1.53632L11.8373 3.14761H12.4763C13.8653 3.14761 15.1664 3.4069 16.3795 3.92548C17.5926 4.44406 18.6529 5.1571 19.5604 6.06462C20.468 6.97213 21.181 8.03244 21.6996 9.24554C22.2182 10.4586 22.4774 11.7597 22.4774 13.1488C22.4774 14.5378 22.2182 15.8389 21.6996 17.052C21.181 18.2651 20.468 19.3254 19.5604 20.2329C18.6529 21.1404 17.5926 21.8535 16.3795 22.372C15.1664 22.8906 13.8653 23.1499 12.4763 23.1499Z" fill="#737D8C"/>
|
||||
<path d="M9.51705 17.0895C9.05682 17.0895 8.64773 17.0107 8.28977 16.853C7.93395 16.6953 7.6527 16.4759 7.44602 16.1946C7.23935 15.9134 7.12962 15.5884 7.11683 15.2198H8.31854C8.32919 15.3967 8.38778 15.5511 8.49432 15.6832C8.60085 15.8132 8.74254 15.9144 8.91939 15.9869C9.09624 16.0593 9.29439 16.0955 9.51385 16.0955C9.74822 16.0955 9.95597 16.055 10.1371 15.9741C10.3182 15.891 10.4599 15.7759 10.5621 15.6289C10.6644 15.4819 10.7145 15.3125 10.7124 15.1207C10.7145 14.9226 10.6634 14.7479 10.5589 14.5966C10.4545 14.4453 10.3033 14.3271 10.1051 14.2418C9.90909 14.1566 9.67259 14.114 9.3956 14.114H8.81712V13.1999H9.3956C9.62358 13.1999 9.8228 13.1605 9.99325 13.0817C10.1658 13.0028 10.3011 12.892 10.3991 12.7493C10.4972 12.6044 10.5451 12.4371 10.543 12.2475C10.5451 12.0621 10.5036 11.9013 10.4183 11.7649C10.3352 11.6264 10.217 11.5188 10.0636 11.4421C9.91229 11.3654 9.73438 11.3271 9.52983 11.3271C9.32955 11.3271 9.14418 11.3633 8.97372 11.4357C8.80327 11.5082 8.66584 11.6115 8.56143 11.7457C8.45703 11.8778 8.40163 12.0355 8.39524 12.2188H7.25426C7.26278 11.8523 7.36825 11.5305 7.57067 11.2536C7.77521 10.9744 8.04794 10.7571 8.38885 10.6016C8.72976 10.4439 9.11222 10.3651 9.53622 10.3651C9.97301 10.3651 10.3523 10.4471 10.674 10.6112C10.9979 10.7731 11.2482 10.9915 11.4251 11.2663C11.6019 11.5412 11.6903 11.8448 11.6903 12.1772C11.6925 12.5458 11.5838 12.8548 11.3643 13.104C11.147 13.3533 10.8615 13.5163 10.5078 13.593V13.6442C10.968 13.7081 11.3207 13.8786 11.5657 14.1555C11.8129 14.4304 11.9354 14.7724 11.9332 15.1815C11.9332 15.5479 11.8288 15.8761 11.62 16.1658C11.4134 16.4535 11.1278 16.6793 10.7635 16.8434C10.4013 17.0075 9.9858 17.0895 9.51705 17.0895ZM15.0151 17.1246C14.4889 17.1246 14.0371 16.9915 13.66 16.7251C13.285 16.4567 12.9963 16.07 12.7939 15.565C12.5936 15.0579 12.4935 14.4474 12.4935 13.7337C12.4956 13.0199 12.5968 12.4126 12.7971 11.9119C12.9995 11.4091 13.2882 11.0256 13.6632 10.7614C14.0403 10.4972 14.491 10.3651 15.0151 10.3651C15.5393 10.3651 15.9899 10.4972 16.367 10.7614C16.7442 11.0256 17.0329 11.4091 17.2332 11.9119C17.4356 12.4148 17.5368 13.022 17.5368 13.7337C17.5368 14.4496 17.4356 15.0611 17.2332 15.5682C17.0329 16.0732 16.7442 16.4588 16.367 16.7251C15.992 16.9915 15.5414 17.1246 15.0151 17.1246ZM15.0151 16.1243C15.4242 16.1243 15.747 15.9229 15.9835 15.5202C16.2222 15.1154 16.3415 14.5199 16.3415 13.7337C16.3415 13.2138 16.2871 12.777 16.1785 12.4233C16.0698 12.0696 15.9164 11.8033 15.7183 11.6243C15.5201 11.4432 15.2857 11.3526 15.0151 11.3526C14.6082 11.3526 14.2864 11.555 14.0499 11.9599C13.8134 12.3626 13.6941 12.9538 13.692 13.7337C13.6898 14.2557 13.742 14.6946 13.8486 15.0504C13.9572 15.4062 14.1107 15.6747 14.3088 15.8558C14.507 16.0348 14.7424 16.1243 15.0151 16.1243Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_forward_30s.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5012 23.1499C15.1497 23.1499 17.4231 22.2702 19.3215 20.5107C21.2198 18.7513 22.2709 16.5751 22.4746 13.9822C22.4931 13.7599 22.4237 13.5655 22.2663 13.3988C22.1088 13.2321 21.9097 13.1488 21.669 13.1488C21.4467 13.1488 21.2569 13.2275 21.0995 13.3849C20.942 13.5423 20.8448 13.7414 20.8078 13.9822C20.604 16.1121 19.715 17.8947 18.1408 19.33C16.5665 20.7654 14.6867 21.4831 12.5012 21.4831C10.1862 21.4831 8.21834 20.6728 6.59778 19.0522C4.97723 17.4317 4.16695 15.4638 4.16695 13.1488C4.16695 10.8337 4.95408 8.86587 6.52833 7.24531C8.10259 5.62475 10.0473 4.81447 12.3623 4.81447H12.9735L11.5289 6.25908C11.3622 6.42577 11.2789 6.62024 11.2789 6.84248C11.2789 7.06473 11.3622 7.2592 11.5289 7.42588C11.6956 7.59257 11.8901 7.67591 12.1123 7.67591C12.3346 7.67591 12.529 7.59257 12.6957 7.42588L15.6127 4.50888C15.7053 4.41628 15.7701 4.32368 15.8072 4.23107C15.8442 4.13847 15.8627 4.0366 15.8627 3.92548C15.8627 3.81436 15.8442 3.71249 15.8072 3.61989C15.7701 3.52729 15.7053 3.43468 15.6127 3.34208L12.6679 0.397297C12.5198 0.249131 12.3346 0.175049 12.1123 0.175049C11.8901 0.175049 11.6956 0.249131 11.5289 0.397297C11.3807 0.563982 11.3067 0.758449 11.3067 0.980697C11.3067 1.20294 11.3807 1.38815 11.5289 1.53632L13.1402 3.14761H12.5012C11.1122 3.14761 9.81112 3.4069 8.59802 3.92548C7.38491 4.44406 6.32461 5.1571 5.41709 6.06462C4.50958 6.97213 3.79654 8.03244 3.27796 9.24554C2.75938 10.4586 2.50009 11.7597 2.50009 13.1488C2.50009 14.5378 2.75938 15.8389 3.27796 17.052C3.79654 18.2651 4.50958 19.3254 5.41709 20.2329C6.32461 21.1404 7.38491 21.8535 8.59802 22.372C9.81112 22.8906 11.1122 23.1499 12.5012 23.1499Z" fill="#737D8C"/>
|
||||
<path d="M9.51705 17.0895C9.05682 17.0895 8.64773 17.0107 8.28977 16.853C7.93395 16.6953 7.6527 16.4759 7.44602 16.1946C7.23935 15.9134 7.12962 15.5884 7.11683 15.2198H8.31854C8.32919 15.3967 8.38778 15.5511 8.49432 15.6832C8.60085 15.8132 8.74254 15.9144 8.91939 15.9869C9.09624 16.0593 9.29439 16.0955 9.51385 16.0955C9.74822 16.0955 9.95597 16.055 10.1371 15.9741C10.3182 15.891 10.4599 15.7759 10.5621 15.6289C10.6644 15.4819 10.7145 15.3125 10.7124 15.1207C10.7145 14.9226 10.6634 14.7479 10.5589 14.5966C10.4545 14.4453 10.3033 14.3271 10.1051 14.2418C9.90909 14.1566 9.67259 14.114 9.3956 14.114H8.81712V13.1999H9.3956C9.62358 13.1999 9.8228 13.1605 9.99325 13.0817C10.1658 13.0028 10.3011 12.892 10.3991 12.7493C10.4972 12.6044 10.5451 12.4371 10.543 12.2475C10.5451 12.0621 10.5036 11.9013 10.4183 11.7649C10.3352 11.6264 10.217 11.5188 10.0636 11.4421C9.91229 11.3654 9.73438 11.3271 9.52983 11.3271C9.32955 11.3271 9.14418 11.3633 8.97372 11.4357C8.80327 11.5082 8.66584 11.6115 8.56143 11.7457C8.45703 11.8778 8.40163 12.0355 8.39524 12.2188H7.25426C7.26278 11.8523 7.36825 11.5305 7.57067 11.2536C7.77521 10.9744 8.04794 10.7571 8.38885 10.6016C8.72976 10.4439 9.11222 10.3651 9.53622 10.3651C9.97301 10.3651 10.3523 10.4471 10.674 10.6112C10.9979 10.7731 11.2482 10.9915 11.4251 11.2663C11.6019 11.5412 11.6903 11.8448 11.6903 12.1772C11.6925 12.5458 11.5838 12.8548 11.3643 13.104C11.147 13.3533 10.8615 13.5163 10.5078 13.593V13.6442C10.968 13.7081 11.3207 13.8786 11.5657 14.1555C11.8129 14.4304 11.9354 14.7724 11.9332 15.1815C11.9332 15.5479 11.8288 15.8761 11.62 16.1658C11.4134 16.4535 11.1278 16.6793 10.7635 16.8434C10.4013 17.0075 9.9858 17.0895 9.51705 17.0895ZM15.0151 17.1246C14.4889 17.1246 14.0371 16.9915 13.66 16.7251C13.285 16.4567 12.9963 16.07 12.7939 15.565C12.5936 15.0579 12.4935 14.4474 12.4935 13.7337C12.4956 13.0199 12.5968 12.4126 12.7971 11.9119C12.9995 11.4091 13.2882 11.0256 13.6632 10.7614C14.0403 10.4972 14.491 10.3651 15.0151 10.3651C15.5393 10.3651 15.9899 10.4972 16.367 10.7614C16.7442 11.0256 17.0329 11.4091 17.2332 11.9119C17.4356 12.4148 17.5368 13.022 17.5368 13.7337C17.5368 14.4496 17.4356 15.0611 17.2332 15.5682C17.0329 16.0732 16.7442 16.4588 16.367 16.7251C15.992 16.9915 15.5414 17.1246 15.0151 17.1246ZM15.0151 16.1243C15.4242 16.1243 15.747 15.9229 15.9835 15.5202C16.2222 15.1154 16.3415 14.5199 16.3415 13.7337C16.3415 13.2138 16.2871 12.777 16.1785 12.4233C16.0698 12.0696 15.9164 11.8033 15.7183 11.6243C15.5201 11.4432 15.2857 11.3526 15.0151 11.3526C14.6082 11.3526 14.2864 11.555 14.0499 11.9599C13.8134 12.3626 13.6941 12.9538 13.692 13.7337C13.6898 14.2557 13.742 14.6946 13.8486 15.0504C13.9572 15.4062 14.1107 15.6747 14.3088 15.8558C14.507 16.0348 14.7424 16.1243 15.0151 16.1243Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_slider_max_track.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="184" height="1" viewBox="0 0 184 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="184" height="1" rx="0.5" fill="#C1C6CD"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 158 B |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_slider_min_track.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="180" height="1" viewBox="0 0 180 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="180" height="1" rx="0.5" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 158 B |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_slider_thumb.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "voice_broadcast_slider_thumb@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "voice_broadcast_slider_thumb@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 813 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -2,16 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "voice_broadcast_spinner.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
@@ -127,3 +127,5 @@
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"LOCATION_FROM_USER" = "%@ hat den eigenen Standort geteilt";
|
||||
|
||||
/* New voice broadcast from a specific person, not referencing a room. */
|
||||
"VOICE_BROADCAST_FROM_USER" = "%@ begann eine Sprachübertragung";
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
// Widget Integration Manager
|
||||
"widget_integration_need_to_be_able_to_invite" = "Du musst Benutzer einladen dürfen, um dies zu tun.";
|
||||
"widget_integration_unable_to_create" = "Erstellen des Widgets nicht möglich.";
|
||||
"widget_integration_failed_to_send_request" = "Senden der Anfrage fehlgeschlagen.";
|
||||
"widget_integration_failed_to_send_request" = "Übertragung der Anfrage fehlgeschlagen.";
|
||||
"widget_integration_room_not_recognised" = "Dieser Raum wurde nicht erkannt.";
|
||||
"widget_integration_positive_power_level" = "Berechtigungslevel muss eine positive Zahl sein.";
|
||||
"widget_integration_must_be_in_room" = "Du bist nicht in diesem Raum.";
|
||||
@@ -922,7 +922,7 @@
|
||||
"manage_session_trusted" = "Von dir vertraut";
|
||||
"manage_session_not_trusted" = "Nicht vertraut";
|
||||
"manage_session_sign_out" = "Von dieser Sitzung abmelden";
|
||||
"widget_picker_manage_integrations" = "Integrationen verwalten…";
|
||||
"widget_picker_manage_integrations" = "Integrationen verwalten …";
|
||||
// Room widget permissions
|
||||
"room_widget_permission_title" = "Widget laden";
|
||||
"room_widget_permission_creator_info_title" = "Dieses Widget wurde hinzugefügt von:";
|
||||
@@ -1506,12 +1506,12 @@
|
||||
"poll_edit_form_add_option" = "Option hinzufügen";
|
||||
"poll_edit_form_option_number" = "Option %lu";
|
||||
"poll_edit_form_question_or_topic" = "Frage oder Thematik";
|
||||
"room_event_action_end_poll" = "Abstimmung beenden";
|
||||
"room_event_action_remove_poll" = "Abstimmung entfernen";
|
||||
"room_event_action_end_poll" = "Umfrage beenden";
|
||||
"room_event_action_remove_poll" = "Umfrage entfernen";
|
||||
|
||||
// Mark: - Polls
|
||||
|
||||
"poll_edit_form_create_poll" = "Abstimmung erstellen";
|
||||
"poll_edit_form_create_poll" = "Umfrage erstellen";
|
||||
"settings_labs_enabled_polls" = "Umfragen";
|
||||
"share_extension_send_now" = "Jetzt senden";
|
||||
"accessibility_button_label" = "Knopf";
|
||||
@@ -1538,7 +1538,7 @@
|
||||
"poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage";
|
||||
"poll_edit_form_input_placeholder" = "Schreib etwas";
|
||||
"analytics_prompt_terms_link_upgrade" = "hier";
|
||||
"poll_timeline_not_closed_title" = "Beenden der Abstimmung fehlgeschlagen";
|
||||
"poll_timeline_not_closed_title" = "Beenden der Umfrage fehlgeschlagen";
|
||||
"poll_timeline_vote_not_registered_subtitle" = "Wir konnten deine Stimme leider nicht erfassen. Versuche es bitte erneut";
|
||||
"poll_timeline_total_final_results" = "Es wurden %lu Stimmen abgegeben";
|
||||
"poll_timeline_total_final_results_one_vote" = "Es wurde 1 Stimme abgegeben";
|
||||
@@ -1547,7 +1547,7 @@
|
||||
"poll_timeline_not_closed_subtitle" = "Versuche es bitte erneut";
|
||||
"poll_timeline_vote_not_registered_title" = "Stimme nicht erfasst";
|
||||
"poll_edit_form_post_failure_subtitle" = "Versuche es bitte erneut";
|
||||
"poll_edit_form_post_failure_title" = "Absenden der Abstimmung fehlgeschlagen";
|
||||
"poll_edit_form_post_failure_title" = "Absenden der Umfrage fehlgeschlagen";
|
||||
"share_extension_low_quality_video_message" = "Für eine bessere Qualität sende es in %@ oder sende es in niedriger Qualität.";
|
||||
"share_extension_low_quality_video_title" = "Das Video wird in niedriger Qualität gesendet werden";
|
||||
"analytics_prompt_stop" = "Teilen beenden";
|
||||
@@ -1590,9 +1590,9 @@
|
||||
"poll_edit_form_update_failure_subtitle" = "Bitte erneut versuchen";
|
||||
"poll_edit_form_poll_type" = "Abstimmungsart";
|
||||
"poll_edit_form_poll_type_closed_description" = "Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest";
|
||||
"poll_edit_form_poll_type_closed" = "Abgeschlossene Abstimmung";
|
||||
"poll_edit_form_poll_type_closed" = "Versteckte Umfrage";
|
||||
"poll_edit_form_poll_type_open_description" = "Abstimmende können die Ergebnisse nach Stimmabgabe sehen";
|
||||
"poll_edit_form_poll_type_open" = "Laufende Abstimmung";
|
||||
"poll_edit_form_poll_type_open" = "Offene Umfrage";
|
||||
"poll_edit_form_update_failure_title" = "Aktualisierung der Umfrage fehlgeschlagen";
|
||||
"threads_empty_tip" = "Hinweis: Tippe auf eine Nachricht und wähle „Thread“ um einen neuen zu starten.";
|
||||
"threads_empty_info_my" = "Antworte auf einen laufenden Thread oder tippe auf eine Nachricht und wähle „Thread“ um einen neuen zu starten.";
|
||||
@@ -2571,7 +2571,6 @@
|
||||
"user_inactive_session_item_with_date" = "Inaktiv seit 90+ Tagen (%@)";
|
||||
"user_inactive_session_item" = "Inaktiv seit 90+ Tagen";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Für besonders sichere Kommunikation verifiziere deine Sitzungen oder melde dich von ihnen ab, falls du sie nicht mehr identifizieren kannst.";
|
||||
"user_other_session_security_recommendation_title" = "Sicherheitsempfehlung";
|
||||
"user_sessions_overview_link_device" = "Verbinde ein Gerät";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2646,7 +2645,7 @@
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Sprachübertragung kann nicht gestartet werden";
|
||||
"settings_labs_enable_voice_broadcast" = "Sprachübertragung (in aktiver Entwicklung)";
|
||||
"settings_labs_enable_voice_broadcast" = "Sprachübertragung";
|
||||
"voice_broadcast_playback_loading_error" = "Wiedergabe der Sprachübertragung nicht möglich.";
|
||||
"deselect_all" = "Alle abwählen";
|
||||
"user_other_session_menu_select_sessions" = "Sitzungen auswählen";
|
||||
@@ -2670,3 +2669,36 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Du hast nicht verifizierte Sitzungen";
|
||||
"launch_loading_processing_response" = "Verarbeite Daten\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synchronisiere mit dem Server\n(%@ Versuch)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Synchronisiere mit dem Server";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Diese Sitzung unterstützt keine Verschlüsselung und kann deshalb nicht verifiziert werden.";
|
||||
"voice_broadcast_time_left" = "%@ übrig";
|
||||
"voice_broadcast_buffering" = "Puffere …";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Ja, beende";
|
||||
"voice_broadcast_stop_alert_description" = "Möchtest du die Übertragung wirklich beenden? Dies wird die Übertragung beenden und die vollständige Aufnahme im Raum bereitstellen.";
|
||||
"voice_broadcast_stop_alert_title" = "Live-Übertragung beenden?";
|
||||
"password_policy_weak_pwd_error" = "Dieses Passwort ist zu schwach. Es muss mindestens 8 Zeichen enthalten, davon mindestens ein Zeichen jeder Art: Großbuchstabe, Kleinbuchstabe, Ziffer und Sonderzeichen.";
|
||||
"password_policy_pwd_in_dict_error" = "Dieses Passwort wurde in einem Wörterbuch gefunden und nicht erlaubt.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Zu kurzes Passwort";
|
||||
"user_session_permanently_unverified_session_description" = "Diese Sitzung unterstützt keine Verschlüsselung, weshalb sie nicht verifiziert werden kann.\n\nDu wirst dich mit dieser Sitzung nicht an Unterhaltungen in Räumen mit aktivierter Verschlüsselung beteiligen können.\n\nAus Sicherheits- und Datenschutzgründen, wird die Nutzung von verschlüsselungsfähigen Matrix-Anwendungen empfohlen.";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Text";
|
||||
"wysiwyg_composer_format_action_link" = "Als Link formatieren";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Link bearbeiten";
|
||||
"wysiwyg_composer_link_action_create_title" = "Link erstellen";
|
||||
"wysiwyg_composer_link_action_link" = "Link";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Als Inline-Code formatieren";
|
||||
"notice_voice_broadcast_ended_by_you" = "Du hast eine Sprachübertragung beendet.";
|
||||
"notice_voice_broadcast_ended" = "%@ beendete eine Sprachübertragung.";
|
||||
"notice_voice_broadcast_live" = "Echtzeitübertragung";
|
||||
"user_other_session_security_recommendation_title" = "Andere Sitzungen";
|
||||
"voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen";
|
||||
"poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden";
|
||||
"voice_message_broadcast_in_progress_message" = "Du kannst kein Gespräch beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen";
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
/* Sticker from a specific person, not referencing a room. */
|
||||
"STICKER_FROM_USER" = "%@ sent a sticker";
|
||||
|
||||
/* New voice broadcast from a specific person, not referencing a room. */
|
||||
"VOICE_BROADCAST_FROM_USER" = "%@ started a voice broadcast";
|
||||
|
||||
/** Notification messages **/
|
||||
|
||||
/* New message indicator on unknown room */
|
||||
|
||||
@@ -802,7 +802,7 @@ Tap the + to start adding people.";
|
||||
"settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager";
|
||||
"settings_labs_enable_new_app_layout" = "New Application Layout";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor";
|
||||
"settings_labs_enable_voice_broadcast" = "Voice broadcast (under active development)";
|
||||
"settings_labs_enable_voice_broadcast" = "Voice broadcast";
|
||||
|
||||
"settings_version" = "Version %@";
|
||||
"settings_olm_version" = "Olm Version %@";
|
||||
@@ -2197,6 +2197,8 @@ Tap the + to start adding people.";
|
||||
"voice_message_remaining_recording_time" = "%@s left";
|
||||
"voice_message_stop_locked_mode_recording" = "Tap on your recording to stop or listen";
|
||||
"voice_message_lock_screen_placeholder" = "Voice message";
|
||||
"voice_message_broadcast_in_progress_title" = "Can't start voice message";
|
||||
"voice_message_broadcast_in_progress_message" = "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message";
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Can't start a new voice broadcast";
|
||||
@@ -2347,6 +2349,10 @@ Tap the + to start adding people.";
|
||||
|
||||
"poll_timeline_not_closed_subtitle" = "Please try again";
|
||||
|
||||
"poll_timeline_decryption_error" = "Due to decryption errors, some votes may not be counted";
|
||||
|
||||
"poll_timeline_ended_text" = "Ended the poll";
|
||||
|
||||
// MARK: - Location sharing
|
||||
|
||||
"location_sharing_title" = "Location";
|
||||
@@ -2474,7 +2480,7 @@ To enable access, tap Settings> Location and select Always";
|
||||
"user_session_rename_session_title" = "Renaming sessions";
|
||||
"user_session_rename_session_description" = "Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.";
|
||||
|
||||
"user_other_session_security_recommendation_title" = "Security recommendation";
|
||||
"user_other_session_security_recommendation_title" = "Other sessions";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore.";
|
||||
"user_other_session_current_session_details" = "Your current session";
|
||||
"user_other_session_verified_sessions_header_subtitle" = "For best security, sign out from any session that you don’t recognize or use anymore.";
|
||||
@@ -2549,6 +2555,14 @@ To enable access, tap Settings> Location and select Always";
|
||||
"wysiwyg_composer_format_action_italic" = "Apply italic format";
|
||||
"wysiwyg_composer_format_action_underline" = "Apply strikethrough format";
|
||||
"wysiwyg_composer_format_action_strikethrough" = "Apply underline format";
|
||||
"wysiwyg_composer_format_action_link" = "Apply link format";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Apply inline code format";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Text";
|
||||
"wysiwyg_composer_link_action_link" = "Link";
|
||||
"wysiwyg_composer_link_action_create_title" = "Create a link";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Edit link";
|
||||
|
||||
// MARK: - MatrixKit
|
||||
|
||||
@@ -2690,6 +2704,9 @@ To enable access, tap Settings> Location and select Always";
|
||||
"notice_crypto_error_unknown_inbound_session_id" = "The sender's session has not sent us the keys for this message.";
|
||||
"notice_sticker" = "sticker";
|
||||
"notice_in_reply_to" = "In reply to";
|
||||
"notice_voice_broadcast_live" = "Live broadcast";
|
||||
"notice_voice_broadcast_ended" = "%@ ended a voice broadcast.";
|
||||
"notice_voice_broadcast_ended_by_you" = "You ended a voice broadcast.";
|
||||
|
||||
// room display name
|
||||
"room_displayname_empty_room" = "Empty room";
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"NSCalendarsUsageDescription" = "Vidu viajn planitajn renkontiĝojn en la aplikaĵo.";
|
||||
"NSContactsUsageDescription" = "Por trovi kontaktojn, kiuj jam estas ĉe Matrix, Element povas sendi retpoŝtadresojn kaj telefonnumerojn el via adresaro al via elektita identigila servilo de Matrix. Kiam eblas, personaj datumoj estas haketitaj antaŭ forsendo – bonvolu kontroli la politikon pri privateco de via identiga servilo por pliaj detaloj.";
|
||||
"NSMicrophoneUsageDescription" = "La mikrofono estas uzata por filmi, kaj ankaŭ por voki.";
|
||||
"NSPhotoLibraryUsageDescription" = "La fotujo estas uzata por sendi fotojn kaj filmojn.";
|
||||
"NSPhotoLibraryUsageDescription" = "Permesu aliron al fotoj por alŝuti fotojn kaj filmetojn el via biblioteko.";
|
||||
// Permissions usage explanations
|
||||
"NSCameraUsageDescription" = "La filmilo estas uzata por foti kaj filmi, kaj ankaŭ por vidvoki.";
|
||||
"NSCameraUsageDescription" = "La filmilo estas uzata por fari vidvokojn, aŭ preni kaj alŝuti fotojn kaj filmetojn.";
|
||||
|
||||
@@ -589,7 +589,7 @@
|
||||
"create_room_section_footer_encryption" = "Ne eblas malŝalti ĉifradon poste.";
|
||||
"create_room_enable_encryption" = "Ŝalti ĉifradon";
|
||||
"create_room_section_header_encryption" = "Ĉifrado de ĉambro";
|
||||
"create_room_placeholder_topic" = "Temo";
|
||||
"create_room_placeholder_topic" = "Pri kio temas ĉi tiu ĉambro?";
|
||||
"create_room_section_header_topic" = "Temo de ĉambro (nedeviga)";
|
||||
"create_room_placeholder_name" = "Nomo";
|
||||
"create_room_section_header_name" = "Nomo de ĉambro";
|
||||
@@ -2032,3 +2032,4 @@
|
||||
"login_error_resource_limit_exceeded_message_default" = "Ĉi tiu hejmservilo atingis unu el siaj rimedaj limoj.";
|
||||
"login_error_resource_limit_exceeded_title" = "Rimeda limo estas atingita";
|
||||
"login_error_forgot_password_is_not_supported" = "Forgesado de pasvorto nun ne estas subtenata";
|
||||
"identity_server_settings_alert_change" = "Ĉu malkonektu de identiga servilo %1$@ kaj anstataŭe konektu al %2$@?";
|
||||
|
||||
@@ -2545,7 +2545,6 @@
|
||||
"user_other_session_verified_sessions_header_subtitle" = "Parima turvalisuse nimel logi välja neist sessioonidest, mida sa enam ei kasuta või ei tunne ära.";
|
||||
"user_other_session_current_session_details" = "Sinu praegune sessioon";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning logi neist välja, mida sa enam ei kasuta või ei tunne enam ära.";
|
||||
"user_other_session_security_recommendation_title" = "Turvalisusega seotud soovitused";
|
||||
"user_other_session_verified_additional_info" = "See sessioon on valmis turvaliseks sõnumivahetuseks.";
|
||||
"user_other_session_unverified_additional_info" = "Parima turvalisuse ja töökindluse nimel verifitseeri see sessioon või logi ta võrgust välja.";
|
||||
"user_session_verification_unknown_additional_info" = "Selle sessiooni olekut ei saa tuvastada enne kui oled ta verifitseerinud.";
|
||||
@@ -2574,7 +2573,7 @@
|
||||
/* The placeholder will be replaces with manage_session_name_info_link */
|
||||
"manage_session_name_info" = "Palun arvesta, et sessioonide nimed on näha ka kõikidele osapooltele, kellega sa suhtled. %@";
|
||||
"manage_session_name_hint" = "Sinu enda kirjutatud sessiooninimede alusel on sul oma seadmeid lihtsam ära tunda.";
|
||||
"settings_labs_enable_voice_broadcast" = "Ringhäälingukõne (aktiivses arenduses)";
|
||||
"settings_labs_enable_voice_broadcast" = "Ringhäälingukõne";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Proovi vormindatud teksti alusel töötavat tekstitoimetit";
|
||||
"authentication_qr_login_failure_retry" = "Proovi uuesti";
|
||||
"authentication_qr_login_failure_request_timed_out" = "Sidumine ei lõppenud etteantud aja jooksul.";
|
||||
@@ -2608,3 +2607,36 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Sul on verifitseerimata sessioone";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi.";
|
||||
"voice_broadcast_time_left" = "aega jäänud %@";
|
||||
"launch_loading_processing_response" = "Töötleme andmeid\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sünkroniseerime andmeid serveriga\n(katse: %@)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sünkroniseerimine serveriga";
|
||||
"voice_broadcast_buffering" = "Andmed on puhverdamisel…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Jah, lõpetame";
|
||||
"voice_broadcast_stop_alert_description" = "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.";
|
||||
"voice_broadcast_stop_alert_title" = "Kas lõpetame otseeetri?";
|
||||
"password_policy_pwd_in_dict_error" = "See salasõna leidub levinud salasõnade sõnastikus ning seda sa ei saa kasutada.";
|
||||
"password_policy_weak_pwd_error" = "See salasõna on liiga lihtne. Ta peaks olema vähemalt 8 tähemärki pikk ning seal peaks leiduma vähemalt üks väiketäht, suurtäht, number ja erimärk.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Liiga lühike salasõna";
|
||||
"user_session_permanently_unverified_session_description" = "Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi.\n\nSelle sessiooniga ei saa sa osaleda krüptitud jututubades.\n\nParima turvalisuse ja privaatsuse nimel palun kasuta selliseid Matrix'i kliente, mis toetavad krüptimist.";
|
||||
"wysiwyg_composer_format_action_link" = "Muuda lingi vormingut";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Tekst";
|
||||
"wysiwyg_composer_link_action_link" = "Link";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Muuda linki";
|
||||
"wysiwyg_composer_link_action_create_title" = "Loo link";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Kasuta lõimitud koodi vormingut";
|
||||
"notice_voice_broadcast_ended_by_you" = "Sa lõpetasid ringhäälingukõne.";
|
||||
"notice_voice_broadcast_ended" = "%@ lõpetas ringhäälingukõne.";
|
||||
"notice_voice_broadcast_live" = "Ringhäälingukõne on eetris";
|
||||
"user_other_session_security_recommendation_title" = "Muud sessioonid";
|
||||
"poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata";
|
||||
"voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu";
|
||||
"voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne";
|
||||
|
||||
@@ -2542,7 +2542,6 @@
|
||||
// First item is client name and second item is session display name
|
||||
"user_session_name" = "%@ : %@";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Vérifiez vos sessions pour renforcer la sécurité de votre messagerie, ou déconnectez celles que vous ne reconnaissez ou utilisez plus.";
|
||||
"user_other_session_security_recommendation_title" = "Recommandations de sécurité";
|
||||
"user_session_push_notifications_message" = "Lorsqu’activé, cette session recevra des notifications push.";
|
||||
"user_session_push_notifications" = "Notifications push";
|
||||
"user_sessions_overview_current_session_section_title" = "Session courante";
|
||||
|
||||
@@ -2493,7 +2493,7 @@
|
||||
|
||||
// First item is client name and second item is session display name
|
||||
"user_session_name" = "%@: %@";
|
||||
"user_session_unverified_additional_info" = "Az aktuális munkamenet készen áll a biztonságos üzenetküldésre.";
|
||||
"user_session_unverified_additional_info" = "Ellenőrizd az aktuális munkamenetet a biztonságos üzenetküldéshez.";
|
||||
"user_session_verified_additional_info" = "Az aktuális munkamenet készen áll a biztonságos üzenetküldésre.";
|
||||
"user_session_learn_more" = "Tudj meg többet";
|
||||
"user_session_view_details" = "Részletek megtekintése";
|
||||
@@ -2565,7 +2565,6 @@
|
||||
"user_other_session_verified_sessions_header_subtitle" = "A legjobb biztonság érdekében jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz.";
|
||||
"user_other_session_current_session_details" = "Jelenlegi munkamenet";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Erősítse meg a munkameneteit a még biztonságosabb csevegéshez vagy jelentkezzen ki ezekből, ha nem ismeri fel vagy már nem használja őket.";
|
||||
"user_other_session_security_recommendation_title" = "Biztonsági javaslat";
|
||||
"user_session_push_notifications_message" = "Ha be van kapcsolva az eszközre Push értesítések lesznek küldve.";
|
||||
"user_session_push_notifications" = "Push értesítések";
|
||||
"user_other_session_verified_additional_info" = "Ez a munkamenet beállítva a biztonságos üzenetküldéshez.";
|
||||
@@ -2625,7 +2624,7 @@
|
||||
"authentication_qr_login_start_subtitle" = "Használd a kamerát ezen az eszközön a másik eszközödön megjelenő QR kód beolvasására:";
|
||||
"authentication_qr_login_start_title" = "QR kód beolvasása";
|
||||
"authentication_login_with_qr" = "Belépés QR kóddal";
|
||||
"settings_labs_enable_voice_broadcast" = "Hang közvetítés (aktív fejlesztés alatt)";
|
||||
"settings_labs_enable_voice_broadcast" = "Hang közvetítés";
|
||||
"wysiwyg_composer_start_action_voice_broadcast" = "Hang közvetítés";
|
||||
"voice_broadcast_playback_loading_error" = "A hang közvetítés nem játszható le.";
|
||||
"voice_broadcast_already_in_progress_message" = "Egy hang közvetítés már folyamatban van. Először fejezd be a jelenlegi közvetítést egy új indításához.";
|
||||
@@ -2656,3 +2655,36 @@
|
||||
"voice_broadcast_tile" = "Hang közvetítés";
|
||||
"voice_broadcast_live" = "Élő";
|
||||
"key_verification_alert_body" = "Tekintsd át, hogy meggyőződj arról, hogy a fiókod biztonságban van.";
|
||||
"user_session_permanently_unverified_session_description" = "Ez a munkamenet nem támogatja a titkosítást, így nem lehet ellenőrizni sem.\n\nEzzel a munkamenettel nem tudsz részt venni olyan szobákban ahol a titkosítás be van kapcsolva.\n\nA biztonság és a adatbiztonsági okokból javasolt olyan Matrix kliens használata ami támogatja a titkosítást.";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Ez a munkamenet nem támogatja a titkosítást, így nem lehet ellenőrizni sem.";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Igen, befejez";
|
||||
"voice_broadcast_stop_alert_description" = "Biztos, hogy befejezed az élő közvetítést? Ez befejezi a közvetítést és a felvétel az egész szoba számára elérhető lesz.";
|
||||
"voice_broadcast_stop_alert_title" = "Megszakítod az élő közvetítést?";
|
||||
"voice_broadcast_buffering" = "Pufferelés…";
|
||||
"voice_broadcast_time_left" = "%@ van vissza";
|
||||
"launch_loading_processing_response" = "Adat feldolgozása\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Szinkronizálás a szerverrel\n(%@ próbálkozás)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Szinkronizálás a szerverrel";
|
||||
"password_policy_pwd_in_dict_error" = "Ez a jelszó megtalálható a szótárban ezért nem engedélyezett.";
|
||||
"password_policy_weak_pwd_error" = "Ez a jelszó túl gyenge. Legalább 8 karakternek kell lennie és minden típusból legalább egy: nagybetű, kisbetű, szám és speciális karakter.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "A jelszó túl rövid";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Hivatkozás szerkesztése";
|
||||
"wysiwyg_composer_link_action_create_title" = "Hivatkozás készítése";
|
||||
"wysiwyg_composer_link_action_link" = "Hivatkozás";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Szöveg";
|
||||
"wysiwyg_composer_format_action_link" = "Hivatkozás";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Beágyazott kód formátum alkalmazása";
|
||||
"notice_voice_broadcast_ended_by_you" = "A hang közvetítést befejezted.";
|
||||
"notice_voice_broadcast_ended" = "%@ befejezte a hang közvetítést.";
|
||||
"notice_voice_broadcast_live" = "Élő közvetítés";
|
||||
"user_other_session_security_recommendation_title" = "További munkamenetek";
|
||||
"poll_timeline_decryption_error" = "Visszafejtési hibák miatt néhány szavazat nem kerül beszámításra";
|
||||
"voice_message_broadcast_in_progress_message" = "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához";
|
||||
"voice_message_broadcast_in_progress_title" = "Hang üzenetet nem lehet elindítani";
|
||||
|
||||
@@ -172,3 +172,6 @@
|
||||
|
||||
/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */
|
||||
"MSGS_FROM_TWO_PLUS_USERS" = "%@ pesan baru dari %@, %@ dan lainnya";
|
||||
|
||||
/* New voice broadcast from a specific person, not referencing a room. */
|
||||
"VOICE_BROADCAST_FROM_USER" = "%@ memulai sebuah siaran suara";
|
||||
|
||||
@@ -690,7 +690,7 @@
|
||||
"identity_server_settings_description" = "Anda saat ini menggunakan %@ untuk menemukan dan dapat ditemukan oleh kontak yang Anda tahu.";
|
||||
|
||||
// AuthenticatedSessionViewControllerFactory
|
||||
"authenticated_session_flow_not_supported" = "Aplikasi ini tidak mendukung mekanisme otentikasi di homeserver Anda.";
|
||||
"authenticated_session_flow_not_supported" = "Aplikasi ini tidak mendukung mekanisme autentikasi di homeserver Anda.";
|
||||
"security_settings_coming_soon" = "Maaf. Aksi ini belum tersedia di %@ iOS. Mohon gunakan klien Matrix yang lain untuk menyiapkannya. %@ iOS akan menggunakannya.";
|
||||
"security_settings_blacklist_unverified_devices_description" = "Verifikasi semua sesi pengguna untuk menandainya sebagai terpercaya dan kirim pesan ke mereka.";
|
||||
"security_settings_crosssigning_info_exists" = "Akun Anda memiliki identitas penandatanganan silang, tetapi belum dipercayai oleh sesi ini. Selesaikan keamanan sesi ini.";
|
||||
@@ -765,7 +765,7 @@
|
||||
"room_intro_cell_information_dm_sentence1_part1" = "Ini adalah awal dari pesan langsung Anda dengan ";
|
||||
"room_intro_cell_information_room_without_topic_sentence2_part2" = " supaya orang tahu tentang ruangan ini.";
|
||||
"invite_friends_share_text" = "Hai, bicara kepada saya di %@: %@";
|
||||
"biometrics_usage_reason" = "Otentikasi diperlukan untuk mengakses aplikasi ini";
|
||||
"biometrics_usage_reason" = "Autentikasi diperlukan untuk mengakses aplikasi ini";
|
||||
"pin_protection_kick_user_alert_message" = "Terlalu banyak kesalahan, Anda telah dikeluarkan";
|
||||
"secrets_setup_recovery_passphrase_confirm_information" = "Masukkan Frasa Keamanan Anda untuk mengkonfirmasi.";
|
||||
"secrets_recovery_with_key_information_verify_device" = "Gunakan Kunci Keamanan Anda untuk memverifikasi perangkat ini.";
|
||||
@@ -792,7 +792,7 @@
|
||||
"bug_crash_report_description" = "Jelaskan apa yang Anda lakukan sebelum crashnya:";
|
||||
"directory_server_type_homeserver" = "Ketik sebuah homeserver untuk menampilkan daftar ruangan publik";
|
||||
"room_details_access_section_anyone_apart_from_guest" = "Siapa saja yang tahu tautan ruangan, selain dari tamu";
|
||||
"security_settings_complete_security_alert_message" = "Anda seharusnya menyelesaikan keamanan di sesi Anda saat ini dulu.";
|
||||
"security_settings_complete_security_alert_message" = "Anda seharusnya menyelesaikan keamanan di sesi Anda saat ini dahulu.";
|
||||
"key_verification_scan_confirmation_scanned_device_information" = "Apakah perangkat yang lain menampilkan perisai yang sama?";
|
||||
"key_verification_verify_qr_code_information" = "Pindai kodenya untuk memverifikasi dengan sesama dengan aman.";
|
||||
"error_not_supported_on_mobile" = "Anda tidak melakukannya dari %@ mobile.";
|
||||
@@ -848,7 +848,7 @@
|
||||
"key_verification_scan_confirmation_scanned_user_information" = "Apakah %@ menampilkan perisai yang sama?";
|
||||
"key_verification_verify_qr_code_scan_other_code_success_message" = "Kode QR berhasil divalidasi.";
|
||||
"key_verification_verify_qr_code_information_other_device" = "Pindai kode di bawah untuk memverifikasi:";
|
||||
"key_verification_bootstrap_not_setup_message" = "Anda harus mem-bootstrap penandatanganan-silang dulu.";
|
||||
"key_verification_bootstrap_not_setup_message" = "Anda harus mem-bootstrap penandatanganan silang dahulu.";
|
||||
"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Memeriksa untuk kemampuan verifikasi lain ...";
|
||||
"key_verification_self_verify_current_session_alert_message" = "Pengguna yang lain mungkin tidak mempercayainya.";
|
||||
"device_verification_cancelled" = "Pengguna yang lain membatalkan verifikasinya.";
|
||||
@@ -1549,7 +1549,7 @@
|
||||
"settings_discovery_three_pids_management_information_part1" = "Kelola alamat email atau nomor telepon apa saja yang pengguna lain dapat menggunakan untuk menemukan Anda dan menggunakannya untuk mengundang Anda ke ruangan. Tambahkan atau hapus alamat email atau nomor telepon dari daftar ini di ";
|
||||
"room_preview_unlinked_email_warning" = "Undangan ini telah dikirim ke %@, yang tidak diasosiasikan dengan akun ini. Anda mungkin ingin masuk ke akun yang lain, atau tambahkan email ini ke akun Anda.";
|
||||
"unknown_devices_alert" = "Ruangan ini berisi sesi tidak dikenal yang belum diverifikasi.\nIni berarti tidak ada jaminan bahwa sesi tersebut adalah milik pengguna yang mereka klaim.\nKami menyarankan Anda memverifikasinya untuk setiap sesi sebelum melanjutkan, tetapi Anda dapat mengirim ulang pesan tanpa memverifikasi jika Anda ingin.";
|
||||
"room_warning_about_encryption" = "Enkripsi ujung ke ujung masih dalam beta dan mungkin tidak dapat diandalkan.\n\nAnda seharusnya tidak mempercayainya dulu untuk mengamankan data.\n\nPerangkat masih belum dapat mendekripsi riwayat sebelum mereka bergabung ke ruangannya.\n\nPesan terenkripsi masih belum terlihat di klien yang belum mengimplementasikan enkripsi.";
|
||||
"room_warning_about_encryption" = "Enkripsi ujung ke ujung masih dalam beta dan mungkin tidak dapat diandalkan.\n\nAnda seharusnya tidak mempercayainya dahulu untuk mengamankan data.\n\nPerangkat masih belum dapat mendekripsi riwayat sebelum mereka bergabung ke ruangannya.\n\nPesan terenkripsi masih belum terlihat di klien yang belum mengimplementasikan enkripsi.";
|
||||
"auth_add_email_and_phone_warning" = "Pendaftaran dengan email dan nomor telepon sekaligus belum didukung sampai API-nya sudah ada. Hanya nomor telepon yang akan diperhitungkan. Anda dapat menambahkan email Anda di profil Anda di pengaturan.";
|
||||
"auth_reset_password_success_message" = "Kata sandi akun Matrix Anda telah diatur ulang.\n\nAnda telah dikeluarkan dari semua sesi dan tidak akan menerima lagi notifikasi push. Untuk mengaktifkan ulang notifikasi, masuk ulang di setiap perangkat.";
|
||||
"spaces_add_rooms_coming_soon_title" = "Penambahan ruangan akan segera datang";
|
||||
@@ -1754,7 +1754,7 @@
|
||||
"home_context_menu_make_room" = "Pindah ke Ruangan";
|
||||
"home_context_menu_make_dm" = "Pindah ke Orang";
|
||||
"event_formatter_message_deleted" = "Pesan dihapus";
|
||||
"settings_labs_enable_threads" = "Perpesanan utasan";
|
||||
"settings_labs_enable_threads" = "Utasan pesan";
|
||||
"message_from_a_thread" = "Dari sebuah utasan";
|
||||
"threads_empty_show_all_threads" = "Tampilkan semua utasan";
|
||||
"threads_empty_tip" = "Tip: Ketuk pada sebuah pesan dan gunakan “Utasan” untuk memulai yang baru.";
|
||||
@@ -1873,7 +1873,7 @@
|
||||
"login_error_forbidden" = "Nama pengguna/kata sandi tidak absah";
|
||||
"login_error_registration_is_not_supported" = "Pendaftaran saat ini tidak didukung";
|
||||
"login_error_do_not_support_login_flows" = "Saat ini kami tidak mendukung salah satu atau semua alur masuk yang ditentukan oleh homeserver ini";
|
||||
"login_error_no_login_flow" = "Kami gagal untuk menerima informasi otentikasi dari homeserver ini";
|
||||
"login_error_no_login_flow" = "Kami gagal untuk menerima informasi autentikasi dari homeserver ini";
|
||||
"login_error_title" = "Login Gagal";
|
||||
"login_prompt_email_token" = "Harap masukkan token validasi email Anda:";
|
||||
"login_email_placeholder" = "Alamat email";
|
||||
@@ -2205,8 +2205,8 @@
|
||||
"room_no_power_to_create_conference_call" = "Anda membutuhkan izin untuk mengundang untuk memulai konferensi di ruangan ini";
|
||||
"room_left_for_dm" = "Anda keluar";
|
||||
"room_left" = "Anda meninggalkan ruangan ini";
|
||||
"room_error_timeline_event_not_found" = "Aplikasi ini sedang mencoba untuk memuat titik tertenu di linimasa ruangan ini tetapi tidak dapat menemukannya";
|
||||
"room_error_timeline_event_not_found_title" = "Gagal untuk memuat posisi linimasa";
|
||||
"room_error_timeline_event_not_found" = "Aplikasi ini sedang mencoba untuk memuat titik tertentu di lini masa ruangan ini tetapi tidak dapat menemukannya";
|
||||
"room_error_timeline_event_not_found_title" = "Gagal memuat posisi lini masa";
|
||||
"room_error_cannot_load_timeline" = "Gagal untuk memuat linimasa";
|
||||
"room_error_topic_edition_not_authorized" = "Anda tidak diizinkan untuk mengubah topik ruangan ini";
|
||||
"room_error_name_edition_not_authorized" = "Anda tidak diizinkan untuk mengubah nama ruangan ini";
|
||||
@@ -2270,8 +2270,8 @@
|
||||
|
||||
// Encryption information
|
||||
"room_event_encryption_info_title" = "Informasi enkripsi ujung-ke-ujung\n\n";
|
||||
"device_details_delete_prompt_message" = "Operasi ini membutuhkan otentikasi tambahan.\nUntuk melanjutkan, silakan masukkan kata sandi Anda.";
|
||||
"device_details_delete_prompt_title" = "Otentikasi";
|
||||
"device_details_delete_prompt_message" = "Operasi ini membutuhkan autentikasi tambahan.\nUntuk melanjutkan, silakan masukkan kata sandi Anda.";
|
||||
"device_details_delete_prompt_title" = "Autentikasi";
|
||||
"device_details_rename_prompt_message" = "Nama publik sesi dapat dilihat oleh orang yang berkomunikasi dengan Anda";
|
||||
"device_details_rename_prompt_title" = "Nama Sesi";
|
||||
"device_details_last_seen_format" = "%@ @ %@\n";
|
||||
@@ -2752,7 +2752,6 @@
|
||||
"user_inactive_session_item_with_date" = "Tidak aktif selama 90+ hari (%@)";
|
||||
"user_inactive_session_item" = "Tidak aktif selama 90+ hari";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verifikasi sesi Anda untuk perpesanan aman yang terbaik atau keluarkan sesi yang Anda tidak kenal atau gunakan lagi.";
|
||||
"user_other_session_security_recommendation_title" = "Saran keamanan";
|
||||
"user_sessions_overview_link_device" = "Tautkan sebuah perangkat";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2840,7 +2839,7 @@
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Tidak dapat memulai sebuah siaran suara baru";
|
||||
"settings_labs_enable_voice_broadcast" = "Siaran suara (dalam pengembangan aktif)";
|
||||
"settings_labs_enable_voice_broadcast" = "Siaran suara";
|
||||
"deselect_all" = "Batalkan Semua Pilihan";
|
||||
"user_other_session_menu_select_sessions" = "Pilih sesi";
|
||||
"user_other_session_selected_count" = "%@ dipilih";
|
||||
@@ -2863,3 +2862,36 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Anda punya sesi yang belum diverifikasi";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Sesi ini tidak mendukung enkripsi jadi tidak dapat diverifikasi.";
|
||||
"voice_broadcast_time_left" = "Tersisa %@";
|
||||
"launch_loading_processing_response" = "Memroses data\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Menyinkron dengan server\n(%@ percobaan)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Menyinkron dengan server";
|
||||
"voice_broadcast_buffering" = "Memuat…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Ya, batalkan";
|
||||
"voice_broadcast_stop_alert_description" = "Apakah Anda ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siarannya, dan rekamanan lengkap akan tersedia dalam ruangan.";
|
||||
"voice_broadcast_stop_alert_title" = "Berhenti menyiarkan langsung?";
|
||||
"password_policy_pwd_in_dict_error" = "Kata sandi ini telah ditemukan dalam sebuah kamus dan tidak diperbolehkan.";
|
||||
"password_policy_weak_pwd_error" = "Kata sandi ini terlalu lemah. Kata sandi harus berisi setidaknya 8 karakter, dengan setidaknya satu karakter dari setiap jenis: huruf besar, huruf kecil, angka, dan karakter spesial.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Kata sandi terlalu pendek";
|
||||
"user_session_permanently_unverified_session_description" = "Sesi ini tidak mendukung enkripsi, sehingga tidak dapat diverifikasi.\n\nAnda tidak akan dapat berpartisipasi dalan ruangan di mana enkripsi diaktifkan saat menggunakan sesi ini.\n\nUntuk keamanan dan privasi terbaik, disarankan untuk menggunakan klien Matrix yang mendukung enkripsi.";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Suntiing tautan";
|
||||
"wysiwyg_composer_link_action_create_title" = "Buat sebuah tautan";
|
||||
"wysiwyg_composer_link_action_link" = "Tautan";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Teks";
|
||||
"wysiwyg_composer_format_action_link" = "Terapkan format tautan";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Terapkan format kode dalam baris";
|
||||
"notice_voice_broadcast_ended_by_you" = "Anda mengakhiri sebuah siaran suara.";
|
||||
"notice_voice_broadcast_ended" = "%@ mengakhiri sebuah siaran suara.";
|
||||
"notice_voice_broadcast_live" = "Siaran langsung";
|
||||
"user_other_session_security_recommendation_title" = "Sesi lainnya";
|
||||
"poll_timeline_decryption_error" = "Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung";
|
||||
"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara selagi Anda merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara";
|
||||
"voice_message_broadcast_in_progress_title" = "Tidak dapat memulai pesan suara";
|
||||
|
||||
@@ -2525,7 +2525,6 @@
|
||||
"user_inactive_session_item" = "Inattiva da 90+ giorni";
|
||||
"user_inactive_session_item_with_date" = "Inattiva da 90+ giorni (%@)";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verifica le tue sessioni per avere conversazioni più sicure o disconnetti quelle che non riconosci o che non usi più.";
|
||||
"user_other_session_security_recommendation_title" = "Consiglio di sicurezza";
|
||||
"user_sessions_overview_link_device" = "Collega un dispositivo";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2605,7 +2604,7 @@
|
||||
"manage_session_name_info" = "Ricorda che i nomi di sessione sono anche visibili alle persone con cui comunichi. %@";
|
||||
"manage_session_name_hint" = "I nomi di sessione personalizzati possono aiutarti a riconoscere i tuoi dispositivi più facilmente.";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Prova l'editor in rich text";
|
||||
"settings_labs_enable_voice_broadcast" = "Trasmissione vocale (in sviluppo attivo)";
|
||||
"settings_labs_enable_voice_broadcast" = "Trasmissione vocale";
|
||||
"wysiwyg_composer_start_action_voice_broadcast" = "Trasmissione vocale";
|
||||
"voice_broadcast_playback_loading_error" = "Impossibile avviare questa trasmissione vocale.";
|
||||
"voice_broadcast_already_in_progress_message" = "Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.";
|
||||
@@ -2636,3 +2635,33 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Hai sessioni non verificate";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Questa sessione non supporta la crittografia, perciò non può essere verificata.";
|
||||
"voice_broadcast_buffering" = "Buffer...";
|
||||
"voice_broadcast_time_left" = "%@ rimasti";
|
||||
"launch_loading_processing_response" = "Elaborazione dati\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sincronizzazione con il server\n(%@ tentativo)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sincronizzazione con il server";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Sì, ferma";
|
||||
"voice_broadcast_stop_alert_description" = "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.";
|
||||
"voice_broadcast_stop_alert_title" = "Fermare la trasmissione in diretta?";
|
||||
"password_policy_pwd_in_dict_error" = "Questa password è stata trovata in un dizionario, perciò non è permessa.";
|
||||
"password_policy_weak_pwd_error" = "Questa password è troppo debole. Deve contenere almeno 8 caratteri, con almeno un carattere di ogni tipo: maiuscole, minuscole, numeri e caratteri speciali.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Password troppo corta";
|
||||
"user_session_permanently_unverified_session_description" = "Questa sessione non supporta la crittografia, perciò non può essere verificata.\n\nNon potrai partecipare in stanze dove la crittografia è attiva mentre usi questa sessione.\n\nPer maggiore sicurezza e privacy, è consigliabile usare i client di Matrix che supportano la crittografia.";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Modifica collegamento";
|
||||
"wysiwyg_composer_link_action_create_title" = "Crea un collegamento";
|
||||
"wysiwyg_composer_link_action_link" = "Collegamento";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Testo";
|
||||
"wysiwyg_composer_format_action_link" = "Applica formato collegamento";
|
||||
"notice_voice_broadcast_ended_by_you" = "Hai terminato una trasmissione vocale.";
|
||||
"notice_voice_broadcast_ended" = "%@ ha terminato una trasmissione vocale.";
|
||||
"notice_voice_broadcast_live" = "Trasmissione in diretta";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Applica formato codice interlinea";
|
||||
"user_other_session_security_recommendation_title" = "Altre sessioni";
|
||||
|
||||
@@ -2724,7 +2724,6 @@
|
||||
"user_other_session_verified_sessions_header_subtitle" = "Voor de beste beveiliging log je uit bij elke sessie die je niet meer herkent of gebruikt.";
|
||||
"user_other_session_current_session_details" = "Jouw huidige sessie";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verifieer je sessies voor verbeterde beveiligde berichtenuitwisseling of meld je af bij sessies die je niet meer herkent of gebruikt.";
|
||||
"user_other_session_security_recommendation_title" = "Beveiligingsaanbeveling";
|
||||
"user_session_push_notifications_message" = "Indien ingeschakeld, ontvangt deze sessie pushmeldingen.";
|
||||
"user_session_push_notifications" = "Pushmeldingen";
|
||||
"user_other_session_verified_additional_info" = "Deze sessie is klaar voor beveiligde berichtenuitwisseling.";
|
||||
@@ -2775,7 +2774,7 @@
|
||||
/* The placeholder will be replaces with manage_session_name_info_link */
|
||||
"manage_session_name_info" = "Houd er rekening mee dat sessienamen ook zichtbaar zijn voor mensen met wie je communiceert. %@";
|
||||
"manage_session_name_hint" = "Met aangepaste sessienamen kan je jouw apparaten gemakkelijker herkennen.";
|
||||
"settings_labs_enable_voice_broadcast" = "Voice-uitzending (in actieve ontwikkeling)";
|
||||
"settings_labs_enable_voice_broadcast" = "Voice-uitzending";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Probeer de rich-text-editor (platte tekst-modus komt binnenkort)";
|
||||
"settings_labs_enable_new_session_manager" = "Nieuwe sessiemanager";
|
||||
"room_first_message_placeholder" = "Stuur je eerste bericht…";
|
||||
|
||||
@@ -1604,7 +1604,7 @@
|
||||
"home_context_menu_make_room" = "Mover para Salas";
|
||||
"home_context_menu_make_dm" = "Mover para Pessoas";
|
||||
"event_formatter_message_deleted" = "Mensagem deletada";
|
||||
"settings_labs_enable_threads" = "Mensageria com threads";
|
||||
"settings_labs_enable_threads" = "Mensagens com threads";
|
||||
"message_from_a_thread" = "De uma thread";
|
||||
"threads_empty_show_all_threads" = "Mostrar todas as threads";
|
||||
"threads_empty_tip" = "Dica: Toque numa mensagem e use “Thread” para começar uma.";
|
||||
@@ -2526,7 +2526,6 @@
|
||||
"user_inactive_session_item_with_date" = "Inativa por 90+ dias (%@)";
|
||||
"user_inactive_session_item" = "Inativa por 90+ dias";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verifique suas sessões para mensageria de segurança melhorada ou faça signout daquelas que você não reconhece ou usa mais.";
|
||||
"user_other_session_security_recommendation_title" = "Recomendação de segurança";
|
||||
"user_sessions_overview_link_device" = "Linkar um dispositivo";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2614,7 +2613,7 @@
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Não dá para começar um novo broadcast de voz";
|
||||
"settings_labs_enable_voice_broadcast" = "Broadcast de voz (sob desenvolvimento ativo)";
|
||||
"settings_labs_enable_voice_broadcast" = "Broadcast de voz";
|
||||
"deselect_all" = "Desselecionar Todas(os)";
|
||||
"user_other_session_menu_select_sessions" = "Selecionar sessões";
|
||||
"user_other_session_selected_count" = "%@ selecionadas";
|
||||
@@ -2624,3 +2623,46 @@
|
||||
"manage_session_sign_out_other_sessions" = "Fazer signout de todas as outras sessões";
|
||||
"voice_broadcast_tile" = "Broadcast de voz";
|
||||
"voice_broadcast_live" = "Ao vivo";
|
||||
"user_session_rename_session_description" = "Outras(os) usuárias(os) em mensagens diretas e salas a que você se junta são capazes de visualizar uma lista completa de suas sessões.\n\nIsto as/os provê com confiança que elas(es) são estão realmente falando com você, mas também significa que elas(es) veem o nome da sessão que você entrar aqui.";
|
||||
"user_session_rename_session_title" = "Renomear sessões";
|
||||
"user_session_inactive_session_description" = "Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação.\n\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita.";
|
||||
"user_session_inactive_session_title" = "Sessões inativas";
|
||||
"user_session_unverified_session_description" = "Sessões não-verificadas são sessões que você tem feito login com suas credenciais mas não têm sido verificadas cruzado.\n\nVocê devia especialmente se certificar que você reconhece estas sessões já que elas podiam representar um uso não-autorizado de sua conta.";
|
||||
"user_session_unverified_session_title" = "Sessão não-verificada";
|
||||
"user_session_verified_session_description" = "Sessões verificadas são onde quer que você esteja usando Element depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessões verificada.\n\nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.";
|
||||
"user_session_verified_session_title" = "Sessões verificadas";
|
||||
"user_session_got_it" = "Entendido";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Esta sessão não suporta encriptação e assim não pode ser verificada.";
|
||||
"voice_broadcast_time_left" = "%@ restando";
|
||||
"launch_loading_processing_response" = "Processando dados\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sincando com o servidor\n(%@ tentativa)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sincando com o servidor";
|
||||
"key_verification_alert_body" = "Revise para assegurar que sua conta está segura.";
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Você tem sessões não-verificadas";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Sim, parar";
|
||||
"voice_broadcast_stop_alert_description" = "Tem certeza que você quer parar seu broadcast ao vivo? Isto vai terminar o broadcast, e a gravação completa vai estar disponível na sala.";
|
||||
"voice_broadcast_stop_alert_title" = "Parar broadcasting ao vivo?";
|
||||
"voice_broadcast_buffering" = "Buffering…";
|
||||
"user_session_permanently_unverified_session_description" = "Esta sessão não suporta encriptação, então ela não pode ser verificada.\n\nVocê não vai ser capaz de participar em salas onde encriptação é habilitada quando usando esta sessão.\n\nPara a melhor segurança e privacidade, é recomendado usar clientes Matrix que suportam encriptação.";
|
||||
"password_policy_pwd_in_dict_error" = "Esta senha tem sido encontrada em um dicionário, e não é permitida.";
|
||||
"password_policy_weak_pwd_error" = "Esta senha é fraca demais. Ela deve conter ao menos 8 caracteres, com ao menos um caractere de cada tipo: caractere maiúsculo, minúsculo, dígito, e especial.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Senha curta demais";
|
||||
"notice_voice_broadcast_ended_by_you" = "Você terminou um broadcast de voz.";
|
||||
"notice_voice_broadcast_ended" = "%@ terminou um broadcast de voz.";
|
||||
"notice_voice_broadcast_live" = "Broadcast ao vivo";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Editar link";
|
||||
"wysiwyg_composer_link_action_create_title" = "Criar um link";
|
||||
"wysiwyg_composer_link_action_link" = "Link";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Texto";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Aplicar formato de código inline";
|
||||
"wysiwyg_composer_format_action_link" = "Aplicar formato de link";
|
||||
"user_other_session_security_recommendation_title" = "Outras sessões";
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"auth_optional_phone_placeholder" = "Номер телефона (не обязательно)";
|
||||
"auth_phone_placeholder" = "Номер телефона";
|
||||
"auth_repeat_password_placeholder" = "Повторите пароль";
|
||||
"auth_repeat_new_password_placeholder" = "Подтвердите свой новый пароль";
|
||||
"auth_repeat_new_password_placeholder" = "Подтвердите свой новый пароль учётной записи Matrix";
|
||||
"auth_invalid_login_param" = "Неверное имя пользователя и/или пароль";
|
||||
"auth_invalid_user_name" = "Имена пользователей могут содержать только буквы, цифры, точки, дефисы и символы подчеркивания";
|
||||
"auth_invalid_password" = "Пароль слишком короткий (мин. 6 символов)";
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth_untrusted_id_server" = "Этот сервер идентификации не является доверенным";
|
||||
"auth_password_dont_match" = "Пароли не совпадают";
|
||||
"auth_username_in_use" = "Имя пользователя занято";
|
||||
"auth_forgot_password" = "Забыли пароль?";
|
||||
"auth_forgot_password" = "Забыли пароль учётной записи Matrix?";
|
||||
"auth_email_not_found" = "Не удалось отправить email: этот адрес электронной почты не найден";
|
||||
"auth_use_server_options" = "Использовать пользовательские параметры сервера (дополнительно)";
|
||||
"auth_email_validation_message" = "Проверьте электронную почту, чтобы продолжить регистрацию";
|
||||
@@ -73,14 +73,14 @@
|
||||
"auth_msisdn_validation_message" = "Мы отправили SMS с кодом активации. Введите этот код ниже.";
|
||||
"auth_msisdn_validation_error" = "Не удалось проверить номер телефона.";
|
||||
"auth_recaptcha_message" = "Этот домашний сервер хочет проверить, что вы не робот";
|
||||
"auth_reset_password_message" = "Чтобы сбросить пароль, введите адрес электронной почты, связанный с вашей учетной записью:";
|
||||
"auth_reset_password_message" = "Чтобы сбросить пароль учётной записи Matrix, введите адрес электронной почты, связанный с вашей учетной записью:";
|
||||
"auth_reset_password_missing_email" = "Необходимо ввести адрес электронной почты, связанный с вашей учетной записью.";
|
||||
"auth_reset_password_missing_password" = "Необходимо ввести новый пароль.";
|
||||
"auth_reset_password_email_validation_message" = "Письмо было отправлено на %@. После перехода по ссылке из письма, нажмите ниже.";
|
||||
"auth_reset_password_next_step_button" = "Я подтвердил свой адрес электронной почты";
|
||||
"auth_reset_password_error_unauthorized" = "Не удалось проверить адрес электронной почты: убедитесь, что вы нажали на ссылку в письме";
|
||||
"auth_reset_password_error_not_found" = "Ваш адрес электронной почты, кажется, не связан с Matrix ID на этом домашнем сервере.";
|
||||
"auth_reset_password_success_message" = "Ваш пароль был сброшен.\n\nВы вышли со всех сессий и больше не будете получать push-уведомления. Чтобы вновь активировать уведомления, заново авторизуйтесь на каждом устройстве.";
|
||||
"auth_reset_password_success_message" = "Ваш пароль учётной записи Matrix был сброшен.\n\nВы вышли со всех сессий и больше не будете получать push-уведомления. Чтобы вновь активировать уведомления, заново авторизуйтесь на каждом устройстве.";
|
||||
"auth_add_email_and_phone_warning" = "Регистрация с электронной почтой и номером телефона одновременно не поддерживается до тех пор, пока не появится API. Будет учитываться только номер телефона. Вы можете добавить свою электронную почту в свой профиль в настройках.";
|
||||
// Chat creation
|
||||
"room_creation_title" = "Новый чат";
|
||||
@@ -166,8 +166,8 @@
|
||||
"room_creation_appearance" = "Внешний вид";
|
||||
"directory_cell_description" = "%tu комнат";
|
||||
"directory_search_results_title" = "Просмотр результатов поиска";
|
||||
"directory_search_results" = "%tu результатов поиска для %@";
|
||||
"directory_search_results_more_than" = ">%tu результатов поиска для %@";
|
||||
"directory_search_results" = "%1$tu результатов поиска для %2$@";
|
||||
"directory_search_results_more_than" = ">%1$tu результатов поиска для %2$@";
|
||||
"room_participants_invite_malformed_id" = "Неверный идентификатор. Должен быть адрес электронной почты или идентификатор Matrix, например '@thomas:matrix.org'";
|
||||
"room_participants_now" = "сейчас";
|
||||
"room_participants_ago" = "назад";
|
||||
@@ -196,7 +196,7 @@
|
||||
"room_event_action_redact" = "Удалить";
|
||||
"room_event_action_more" = "Больше";
|
||||
"room_event_action_share" = "Поделиться";
|
||||
"room_event_action_permalink" = "Постоянная ссылка";
|
||||
"room_event_action_permalink" = "Скопировать ссылку на сообщение";
|
||||
"room_event_action_view_source" = "Посмотреть источник";
|
||||
"room_event_action_report" = "Сообщить о недопустимом контенте";
|
||||
"room_event_action_report_prompt_reason" = "Причина сообщениея о недопустимом контенте";
|
||||
@@ -504,7 +504,7 @@
|
||||
"room_do_not_have_permission_to_post" = "У вас нет разрешения на публикацию в этой комнате";
|
||||
"settings_flair" = "Покажите настроение, где это разрешено";
|
||||
"room_details_flair_section" = "Показать настроение для сообществ";
|
||||
"room_event_action_kick_prompt_reason" = "Причина по которой этот пользователь будет выкинут";
|
||||
"room_event_action_kick_prompt_reason" = "Причина исключения пользователя";
|
||||
"room_event_action_ban_prompt_reason" = "Причина по которой этот пользователь будет забанен";
|
||||
// GDPR
|
||||
"gdpr_consent_not_given_alert_message" = "Для продолжения использования сервера %@ вы должны принять условия и положения.";
|
||||
@@ -675,7 +675,7 @@
|
||||
"settings_labs_message_reaction" = "Реагировать на сообщения с Emoji";
|
||||
"settings_key_backup_button_connect" = "Подключите этот сеанс к резервному копированию ключей";
|
||||
"close" = "Закрыть";
|
||||
"auth_forgot_password_error_no_configured_identity_server" = "Сервер идентификации не настроен: добавьте один для сброса пароля.";
|
||||
"auth_forgot_password_error_no_configured_identity_server" = "Сервер идентификации не настроен: добавьте один для сброса пароля учётной записи Matrix.";
|
||||
"auth_softlogout_signed_out" = "Вы вышли";
|
||||
"auth_softlogout_sign_in" = "Войти";
|
||||
"auth_softlogout_clear_data" = "Очистить личные данные";
|
||||
@@ -820,9 +820,9 @@
|
||||
"auth_add_email_message_2" = "Установите адрес электронной почты для восстановления учетной записи, а затем ее можно будет найти людям, которые вас знают.";
|
||||
"auth_add_phone_message_2" = "Задайте телефон, и позже его могут найти люди, которые вас знают.";
|
||||
"auth_add_email_phone_message_2" = "Установка адреса электронной почты для восстановления учетной записи. Используйте позже электронную почту или телефон, чтобы люди, которые вас знают, могли их по желанию найти.";
|
||||
"auth_email_is_required" = "Сервер идентификации не настроен, поэтому вы не можете добавить адрес электронной почты, чтобы в будущем сбросить пароль.";
|
||||
"auth_phone_is_required" = "Сервер идентификации не настроен, поэтому вы не можете добавить номер телефона, чтобы в будущем сбросить пароль.";
|
||||
"auth_reset_password_error_is_required" = "Сервер идентификации не настроен: добавьте ID-Сервер в параметры сервера для сброса пароля.";
|
||||
"auth_email_is_required" = "Сервер идентификации не настроен, поэтому вы не можете добавить адрес электронной почты, чтобы в будущем сбросить пароль учётной записи Matrix.";
|
||||
"auth_phone_is_required" = "Сервер идентификации не настроен, поэтому вы не можете добавить номер телефона, чтобы в будущем сбросить пароль учётной записи Matrix.";
|
||||
"auth_reset_password_error_is_required" = "Сервер идентификации не настроен: добавьте ID-Сервер в параметры сервера для сброса пароля учётной записи Matrix.";
|
||||
"room_accessiblity_scroll_to_bottom" = "Прокрутить вниз";
|
||||
"room_accessibility_search" = "Поиск";
|
||||
"room_accessibility_integrations" = "Интеграция";
|
||||
@@ -2168,3 +2168,35 @@
|
||||
"authentication_qr_login_start_subtitle" = "Используйте камеру на этом устройстве, чтобы сканировать QR-код, отображённый на вашем другом устройстве:";
|
||||
"authentication_qr_login_start_title" = "Сканировать QR-код";
|
||||
"authentication_terms_policy_url_error" = "Не получилось найти выбранные правила. Пожалуйста, попробуйте снова позже.";
|
||||
"threads_empty_tip" = "Подсказка: Нажмите на сообщение и используйте «Поток», чтобы начать переписку.";
|
||||
"threads_empty_info_my" = "Ответьте в действующий поток, или нажмите на «Поток», чтобы начать новый.";
|
||||
"threads_empty_info_all" = "Потоки помогают придерживаться темы разговора и легко отслеживаются.";
|
||||
"threads_empty_title" = "Организуйте свои обсуждения при помощи потоков";
|
||||
"room_first_message_placeholder" = "Отправьте своё первое сообщение…";
|
||||
"room_participants_leave_processing" = "Выход";
|
||||
"authentication_qr_login_failure_retry" = "Попробовать снова";
|
||||
"authentication_qr_login_failure_request_timed_out" = "Привязка не была совершена за нужное время.";
|
||||
"authentication_qr_login_failure_request_denied" = "Запрос был отклонён на другом устройстве.";
|
||||
"authentication_qr_login_failure_invalid_qr" = "Недействительный QR-код.";
|
||||
"authentication_qr_login_failure_title" = "Не удалось привязать";
|
||||
"authentication_qr_login_loading_signed_in" = "Вы вошли с другого устройства.";
|
||||
"authentication_qr_login_loading_waiting_signin" = "Ожидание входа устройства.";
|
||||
"authentication_qr_login_loading_connecting_device" = "Соединение с устройством";
|
||||
"authentication_qr_login_confirm_alert" = "Пожалуйста убедитесь в том, что вы знаете о происхождении этого кода. Привязав устройства, вы дадите кому-то полный доступ к вашей учётной записи.";
|
||||
"authentication_qr_login_confirm_subtitle" = "Убедитесь, что код снизу совпадает с кодом на вашем другом устройстве:";
|
||||
"authentication_qr_login_confirm_title" = "Безопасное соединение установлено";
|
||||
"authentication_qr_login_scan_subtitle" = "Разместите QR-код в квадрате снизу";
|
||||
"authentication_qr_login_scan_title" = "Сканировать QR-код";
|
||||
"authentication_qr_login_display_step2" = "Виберите «Войти при помощи QR-кода»";
|
||||
"authentication_qr_login_display_step1" = "Откройте Element на вашем другом устройстве";
|
||||
"authentication_qr_login_display_subtitle" = "Сканируйте QR-код снизу со своего устройства, с которого вы вышли.";
|
||||
"authentication_qr_login_display_title" = "Привязать устройство";
|
||||
"authentication_qr_login_start_display_qr" = "Показать QR-код на этом устройстве";
|
||||
"authentication_qr_login_start_need_alternative" = "Нуждаетесь в альтернативном методе?";
|
||||
"authentication_qr_login_start_step4" = "Выберите «Показать QR-код на этом устройстве»";
|
||||
"password_policy_pwd_in_dict_error" = "Этот пароль был найден в словаре и не разрешен.";
|
||||
"password_policy_weak_pwd_error" = "Этот пароль слишком слаб. Он должен содержать не менее 8 символов, по крайней мере по одному символу каждого типа: прописные, строчные, цифры и специальные символы.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Очень короткий пароль";
|
||||
"settings_enable_room_message_bubbles" = "Сообщения пузырями";
|
||||
|
||||
@@ -2748,7 +2748,6 @@
|
||||
"user_inactive_session_item_with_date" = "Neaktívna viac ako 90 dní (%@)";
|
||||
"user_inactive_session_item" = "Neaktívna viac ako 90 dní";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Overte si relácie pre vylepšené bezpečné zasielanie správ alebo sa odhláste z tých, ktoré už nepoznáte alebo nepoužívate.";
|
||||
"user_other_session_security_recommendation_title" = "Bezpečnostné odporúčania";
|
||||
"user_sessions_overview_link_device" = "Prepojiť zariadenie";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2835,7 +2834,7 @@
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Nie je možné spustiť nové hlasové vysielanie";
|
||||
"settings_labs_enable_voice_broadcast" = "Hlasové vysielanie (v štádiu aktívneho vývoja)";
|
||||
"settings_labs_enable_voice_broadcast" = "Hlasové vysielanie";
|
||||
"voice_broadcast_playback_loading_error" = "Toto hlasové vysielanie nie je možné prehrať.";
|
||||
"deselect_all" = "Zrušiť výber všetkých";
|
||||
"user_other_session_selected_count" = "%@ vybratých";
|
||||
@@ -2859,3 +2858,36 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Máte neoverené relácie";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Táto relácia nepodporuje šifrovanie, a preto ju nemožno overiť.";
|
||||
"voice_broadcast_time_left" = "%@ ostáva";
|
||||
"launch_loading_processing_response" = "Spracovanie údajov\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synchronizácia so serverom\n(%@ pokus)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Synchronizácia so serverom";
|
||||
"voice_broadcast_buffering" = "Načítavanie do vyrovnávacej pamäte…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Áno, zastaviť";
|
||||
"voice_broadcast_stop_alert_description" = "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.";
|
||||
"voice_broadcast_stop_alert_title" = "Zastaviť vysielanie naživo?";
|
||||
"password_policy_pwd_in_dict_error" = "Toto heslo bolo nájdené v slovníku a nie je povolené.";
|
||||
"password_policy_weak_pwd_error" = "Toto heslo je príliš slabé. Musí obsahovať aspoň 8 znakov, pričom musí obsahovať aspoň jeden znak z každého typu: veľké a malé písmená, číslice a špeciálny symbol.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Príliš krátke heslo";
|
||||
"user_session_permanently_unverified_session_description" = "Táto relácia nepodporuje šifrovanie, takže ju nemožno overiť.\n\nPri používaní tejto relácie sa nebudete môcť zúčastňovať konverzácií v miestnostiach, kde je zapnuté šifrovanie.\n\nNa dosiahnutie čo najlepšieho zabezpečenia a súkromia sa odporúča používať Matrix klientov, ktoré podporujú šifrovanie.";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Upraviť odkaz";
|
||||
"wysiwyg_composer_link_action_create_title" = "Vytvoriť odkaz";
|
||||
"wysiwyg_composer_link_action_link" = "Odkaz";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Text";
|
||||
"wysiwyg_composer_format_action_link" = "Použiť formát odkazu";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Použiť formát riadkového kódu";
|
||||
"notice_voice_broadcast_ended_by_you" = "Ukončili ste hlasové vysielanie.";
|
||||
"notice_voice_broadcast_ended" = "%@ ukončil/a hlasové vysielanie.";
|
||||
"notice_voice_broadcast_live" = "Živé vysielanie";
|
||||
"user_other_session_security_recommendation_title" = "Iné relácie";
|
||||
"poll_timeline_decryption_error" = "Z dôvodu chýb v dešifrovaní sa niektoré hlasy nemusia započítať";
|
||||
"voice_message_broadcast_in_progress_message" = "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu";
|
||||
"voice_message_broadcast_in_progress_title" = "Nemožno spustiť hlasovú správu";
|
||||
|
||||
@@ -2486,7 +2486,6 @@
|
||||
"user_other_session_verified_sessions_header_subtitle" = "Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më.";
|
||||
"user_other_session_current_session_details" = "Sesioni juaj i tanishëm";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më.";
|
||||
"user_other_session_security_recommendation_title" = "Rekomandim sigurie";
|
||||
"user_session_push_notifications_message" = "Kur aktivizohet, ky sesion do të marrë njoftime push.";
|
||||
"user_session_push_notifications" = "Njoftime Push";
|
||||
"user_other_session_verified_additional_info" = "Ky sesion është gati për shkëmbim të sigurt mesazhesh.";
|
||||
@@ -2595,8 +2594,8 @@
|
||||
/* The placeholder will be replaces with manage_session_name_info_link */
|
||||
"manage_session_name_info" = "Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni.%@";
|
||||
"manage_session_name_hint" = "Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja.";
|
||||
"settings_labs_enable_voice_broadcast" = "Aktivizoni transmetim zanor (nën zhvillim aktiv)";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti)";
|
||||
"settings_labs_enable_voice_broadcast" = "Aktivizoni transmetim zanor";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Provoni përpunuesin e teksteve të pasur";
|
||||
"settings_labs_enable_new_app_layout" = "Skemë e Re Aplikacioni";
|
||||
"settings_labs_enable_new_client_info_feature" = "Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionit te përgjegjës sesionesh";
|
||||
"settings_labs_enable_new_session_manager" = "Përgjegjës i ri sesionesh";
|
||||
@@ -2630,3 +2629,50 @@
|
||||
"invite_to" = "Ftojeni te %@";
|
||||
"all_chats_empty_list_placeholder_title" = "S’ka gjë tjetër për të parë.";
|
||||
"all_chats_edit_layout_add_filters_message" = "Filtroni automatikisht mesazhet tuaj në kategori që caktoni vetë";
|
||||
"user_session_rename_session_description" = "Përdorues të tjerë në mesazhe të drejtpërdrejtë dhe dhoma ku jeni në gjendje të shihni një listë të plotë të sesioneve tuaja.\n\nKjo u jep besim atyre se po flasin vërtet me ju, por do të thotë edhe se mund të shohin emrin e sesionit që jepni këtu.";
|
||||
"user_session_inactive_session_description" = "Sesione jo aktive janë sesione që keni ca kohë që s’i keni përdorur, por që vazhdojnë të marrin kyçe fshehtëzimi.\n\nHeqja e sesioneve jo aktive përmirëson sigurinë dhe punimin dhe e bëjnë të lehtë për ju të identifikoni, nëse një sesion i ri është i dyshimtë.";
|
||||
"user_session_unverified_session_description" = "Sesione të paverifikuar janë sesione ku keni bërë hyrjen me kredencialet tuaja, por që nuk janë ndër-verifikuar.\n\nDuhet ta bëni veçanërisht të qartë se i njihni këto sesione, ngaqë mund të përfaqësojnë përdorim të paautorizuar të llogarisë tuaj.";
|
||||
"user_session_verified_session_description" = "Sesione të verifikuar janë ata kudo që përdorni Element-in pasi të keni dhënë frazëkalimin tuaj, ose pasi të keni ripohuar identitetin tuaj përmes një tjetër sesioni të verifikuar.\n\nKjo do të thotë se zotëroni krejt kyçet e nevojshëm për të shkyçur mesazhet tuaj të fshehtëzuar dhe ripohuar përdoruesve të tjerë se e besoni këtë sesion.";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Po njëkohësohet me shërbyesin\n(Përpjekja e %@)";
|
||||
"user_session_rename_session_title" = "Riemërtim sesionesh";
|
||||
"user_session_inactive_session_title" = "Sesione jo aktive";
|
||||
"user_session_unverified_session_title" = "Sesione të paverifikuar";
|
||||
"user_session_verified_session_title" = "Sesione të verifikuar";
|
||||
"user_session_got_it" = "E mora vesh";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Ky sesion s’mbulon fshehtëzim, ndaj s’mund të verifikohet.";
|
||||
"user_sessions_hide_location_info" = "Fshihe adresën IP";
|
||||
"user_sessions_show_location_info" = "Shfaq adresë IP";
|
||||
"voice_broadcast_time_left" = "Edhe %@";
|
||||
"voice_broadcast_tile" = "Transmetim zanor";
|
||||
"launch_loading_processing_response" = "Po përpunohen të dhëna\n%@ %%";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Po njëkohësohet me shërbyesin";
|
||||
"key_verification_alert_body" = "Shqyrtojini, që të siguroheni se llogaria juaj është e parrezik.";
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Keni sesione të paverifikuar";
|
||||
"manage_session_sign_out_other_sessions" = "Dilni prej krejt sesioneve të tjerë";
|
||||
"user_other_session_menu_sign_out_sessions" = "Dilni nga %@ sesione";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Po, ndaleni";
|
||||
"voice_broadcast_stop_alert_description" = "Jeni i sigurt se doni të ndalet transmetimi juaj i drejtpërdrejtë? Kjo do të ndalë transmetimin dhe regjistrimi i plotë do të jetë i passhëm te dhoma.";
|
||||
"voice_broadcast_stop_alert_title" = "Të ndalet transmetimi i drejtpërdrejtë?";
|
||||
"password_policy_pwd_in_dict_error" = "Ky fjalëkalim gjendet në një fjalor dhe nuk lejohet.";
|
||||
"password_policy_weak_pwd_error" = "Ky fjalëkalim është shumë i shkurtër. Duhet të përmbajë të paktën 8 shenja, me të paktën një shenjë nga çdo lloj: të mëdha, të vogla, shifra dhe shenja speciale.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Fjalëkalim shumë i shkurtër";
|
||||
"user_session_permanently_unverified_session_description" = "Ky sesion nuk mbulon fshehtëzim, ndaj s’mund të verifikohet.\n\nS’do të jeni në gjendje të merrni pjesë në dhoma ku fshehtëzimi është i aktivizuar, kur përdorni këtë sesion.\n\nPër sigurinë dhe privatësinë më të mirë, rekomandohet të përdorni klientë Matrix që mbulojnë fshehtëzimin.";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Përpunoni një lidhje";
|
||||
"wysiwyg_composer_link_action_create_title" = "Krijoni një lidhje";
|
||||
"wysiwyg_composer_link_action_link" = "Lidhje";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Tekst";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Apliko formatim kodi brendazi";
|
||||
"wysiwyg_composer_format_action_link" = "Apliko formatim lidhjeje";
|
||||
"notice_voice_broadcast_ended_by_you" = "Përfunduar një transmetim zanor.";
|
||||
"notice_voice_broadcast_ended" = "%@ përfundoi një transmetim zanor.";
|
||||
"notice_voice_broadcast_live" = "Transmetim i drejtëpërdrejtë";
|
||||
"user_other_session_security_recommendation_title" = "Sesione të tjerë";
|
||||
|
||||
@@ -95,3 +95,60 @@
|
||||
|
||||
/* New message from a specific person in a named room */
|
||||
"MSG_FROM_USER_IN_ROOM" = "%@ ได้โพสต์ใน %@";
|
||||
|
||||
/* Group call from user, CallKit caller name */
|
||||
"GROUP_CALL_FROM_USER" = "%@ (การโทรแบบกลุ่ม)";
|
||||
|
||||
/* A user added a Jitsi call to a room */
|
||||
"GROUP_CALL_STARTED" = "เริ่มการโทรแบบกลุ่มแล้ว";
|
||||
|
||||
/* A user's membership has updated in an unknown way */
|
||||
"USER_MEMBERSHIP_UPDATED" = "%@ ได้อัพเดตรูปโปรไฟล์";
|
||||
|
||||
/* A user has change their avatar */
|
||||
"USER_UPDATED_AVATAR" = "%@ เปลี่ยนอวาตาร์ของเขา";
|
||||
|
||||
/* A user has change their name to a new name which we don't know */
|
||||
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ เปลี่ยนชื่อของเขา";
|
||||
|
||||
/** Membership Updates **/
|
||||
|
||||
/* A user has change their name to a new name */
|
||||
"USER_UPDATED_DISPLAYNAME" = "%@ เปลี่ยนชื่อเป็น %@";
|
||||
|
||||
/* A user has reacted to a message, but the reaction content is unknown */
|
||||
"GENERIC_REACTION_FROM_USER" = "%@ ส่งความรู้สึก";
|
||||
|
||||
/** Reactions **/
|
||||
|
||||
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
|
||||
"REACTION_FROM_USER" = "%@ รู้สึก %@";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"LOCATION_FROM_USER" = "%@ แบ่งปันตำแหน่งของเขา";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"FILE_FROM_USER" = "%@ ส่งไฟล์ %@";
|
||||
|
||||
/* New voice message from a specific person, not referencing a room. */
|
||||
"VOICE_MESSAGE_FROM_USER" = "%@ ส่งข้อความเสียง";
|
||||
|
||||
/* New audio message from a specific person, not referencing a room. */
|
||||
"AUDIO_FROM_USER" = "%@ ส่งไฟล์เสียง %@";
|
||||
|
||||
/* New video message from a specific person, not referencing a room. */
|
||||
"VIDEO_FROM_USER" = "%@ ส่งวีดีโอ";
|
||||
|
||||
/** Media Messages **/
|
||||
|
||||
/* New image message from a specific person, not referencing a room. */
|
||||
"PICTURE_FROM_USER" = "%@ ส่งรูปภาพ";
|
||||
|
||||
/* New message reply from a specific person in a named room. */
|
||||
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ ตอบกลับใน %@";
|
||||
|
||||
/* New message reply from a specific person, not referencing a room. */
|
||||
"REPLY_FROM_USER_TITLE" = "%@ ตอบกลับ";
|
||||
/** General **/
|
||||
|
||||
"Notification" = "การแจ้งเตือน";
|
||||
|
||||
@@ -2495,7 +2495,7 @@
|
||||
"location_sharing_live_list_item_last_update_invalid" = "Час останнього оновлення невідомий";
|
||||
"location_sharing_live_list_item_last_update" = "Оновлено %@ тому";
|
||||
"location_sharing_live_list_item_sharing_expired" = "Надсилання завершено";
|
||||
"location_sharing_live_list_item_time_left" = "%@ виходить";
|
||||
"location_sharing_live_list_item_time_left" = "Залишилося %@";
|
||||
"location_sharing_live_viewer_title" = "Місце перебування";
|
||||
"location_sharing_live_map_callout_title" = "Поділитися місцем перебування";
|
||||
"room_access_settings_screen_upgrade_alert_note" = "Зауважте, що оновлення створить нову версію кімнати. Усі поточні повідомлення залишаться в цій архівованій кімнаті.";
|
||||
@@ -2750,7 +2750,6 @@
|
||||
"user_inactive_session_item_with_date" = "Неактивний понад 90 днів (%@)";
|
||||
"user_inactive_session_item" = "Неактивний понад 90 днів";
|
||||
"user_other_session_unverified_sessions_header_subtitle" = "Перевірте свої сеанси для посилення безпеки обміну повідомленнями або вийдіть з тих, які ви більше не розпізнаєте або не використовуєте.";
|
||||
"user_other_session_security_recommendation_title" = "Поради з безпеки";
|
||||
"user_sessions_overview_link_device" = "Пов'язати пристрій";
|
||||
|
||||
// MARK: User sessions management
|
||||
@@ -2838,7 +2837,7 @@
|
||||
|
||||
// Mark: - Voice broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Не вдалося розпочати нову голосову трансляцію";
|
||||
"settings_labs_enable_voice_broadcast" = "Голосові трансляції (в активній розробці)";
|
||||
"settings_labs_enable_voice_broadcast" = "Голосові трансляції";
|
||||
"deselect_all" = "Скасувати вибір усіх";
|
||||
"user_other_session_menu_select_sessions" = "Вибрати сеанси";
|
||||
"user_other_session_selected_count" = "Вибрано %@";
|
||||
@@ -2861,3 +2860,36 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "У вас є не звірені сеанси";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Цей сеанс не підтримує шифрування, і його не можна звірити.";
|
||||
"voice_broadcast_time_left" = "Залишилося %@";
|
||||
"launch_loading_processing_response" = "Обробка даних\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Синхронізація з сервером\n(%@ спроба)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Синхронізація з сервером";
|
||||
"voice_broadcast_buffering" = "Буферизація...";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Так, припинити";
|
||||
"voice_broadcast_stop_alert_description" = "Ви впевнені, що хочете припинити голосову трансляцію? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.";
|
||||
"voice_broadcast_stop_alert_title" = "Припинити голосову трансляцію?";
|
||||
"password_policy_pwd_in_dict_error" = "Цей пароль знайдений у словнику і недопустимий.";
|
||||
"password_policy_weak_pwd_error" = "Цей пароль занадто слабкий. Він повинен містити щонайменше 8 символів, причому хоча б по одному символу кожного типу: великі букви, малі букви, цифри та спеціальні символи.";
|
||||
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Пароль закороткий";
|
||||
"user_session_permanently_unverified_session_description" = "Цей сеанс не підтримує шифрування, тому його неможливо звірити.\n\nПід час користування цим сеансом ви не зможете брати участь у кімнатах, в яких увімкнено шифрування.\n\nДля найкращої безпеки та приватності радимо користуватися клієнтами Matrix, які підтримують шифрування.";
|
||||
"wysiwyg_composer_link_action_edit_title" = "Змінити посилання";
|
||||
"wysiwyg_composer_link_action_create_title" = "Створити посилання";
|
||||
"wysiwyg_composer_link_action_link" = "Посилання";
|
||||
|
||||
// Links
|
||||
"wysiwyg_composer_link_action_text" = "Текст";
|
||||
"wysiwyg_composer_format_action_link" = "Застосувати формат посилання";
|
||||
"wysiwyg_composer_format_action_inline_code" = "Застосовувати вбудований формат коду";
|
||||
"notice_voice_broadcast_ended_by_you" = "Ви завершили голосову трансляцію.";
|
||||
"notice_voice_broadcast_ended" = "%@ завершує голосову трансляцію.";
|
||||
"notice_voice_broadcast_live" = "Трансляція наживо";
|
||||
"user_other_session_security_recommendation_title" = "Інші сеанси";
|
||||
"poll_timeline_decryption_error" = "Через помилки під час розшифрування деякі голоси можуть бути не враховані";
|
||||
"voice_message_broadcast_in_progress_title" = "Неможливо розпочати запис голосового повідомлення";
|
||||
"voice_message_broadcast_in_progress_message" = "Ви не можете розпочати запис голосового повідомлення, оскільки зараз триває запис трансляції наживо. Будь ласка, завершіть трансляцію, щоб розпочати запис голосового повідомлення";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"cancel" = "取消";
|
||||
"save" = "保存";
|
||||
"join" = "加入";
|
||||
"decline" = "取消";
|
||||
"decline" = "拒绝";
|
||||
"accept" = "接受";
|
||||
"preview" = "预览";
|
||||
"camera" = "摄像头";
|
||||
@@ -522,7 +522,7 @@
|
||||
"room_resource_usage_limit_reached_message_contact_3" = " 以提高限制。";
|
||||
// String for App Store
|
||||
"store_short_description" = "安全、去中心化的聊天及 VoIP 应用";
|
||||
"store_full_description" = "Element 是一种新型的通讯与协作应用:\n\n1. 使您可以掌控您的隐私\n2. 使您与 Matrix 网络中的任何人交流,甚至可以通过集成功能与如 Slack 之类的其他应用通讯\n3. 保护您免受广告,大数据挖掘和封闭服务的侵害\n4. 通过端到端加密保证安全,通过交叉签名验证其他人\n\nElement 与其他通讯与协作应用完全不同,因为它是去中心化且开源的。\n\nElement 允许您自托管——或者选择托管商——因此,您能拥有数据和会话的隐私权,所有权和控制权。它允许您访问开放网络;因此,您可以与 Element 用户以外的人交流。并且它非常安全。\n\nElement 之所以可以做到这些,是因为它在 Matrix 上运行——开放,去中心化通讯的标准。\n\n通过让您选择由谁来托管您的会话,Element 让您掌控一切。在 Element 应用中,您可以选择不同的托管方式:\n\n1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费账户,或从志愿者托管的上千个公共服务器中选择\n2. 在您自己的硬件上运行服务器,自托管您的会话\n3. 通过订阅 Element Matrix Services 托管平台,简单地在自定义服务器上注册账户\n\n为什么选择 Element?\n\n掌控您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。\n\n开放通讯与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至/即使他们在使用不同的通讯系统,例如 Slack,IRC 或 XMPP。\n\n超级安全:支持真正的端到端加密(仅有会话中的人可以解密消息),还有能够验证会话参与方的设备的交叉签名。\n\n完善的通讯方式:消息,语音和视频通话,文件共享,屏幕共享和大量集成功能,机器人和挂件。建立房间与社区,保持联系并完成工作。\n\n随时随地:消息历史可在您的全部设备和 https://app.element.io 网页端之间完全同步,无论您在哪里,都可以保持联系。";
|
||||
"store_full_description" = "Element 是一种新型的通讯与协作应用:\n\n1. 使您可以掌控您的隐私\n2. 使您与 Matrix 网络中的任何人交流,甚至可以通过集成功能与如 Slack 之类的其他应用通讯\n3. 保护您免受广告,大数据挖掘和封闭服务的侵害\n4. 通过端到端加密保证安全,通过交叉签名验证其他人\n\nElement 与其他通讯与协作应用完全不同,因为它是去中心化且开源的。\n\nElement 允许您自托管——或者选择托管商——因此,您能拥有数据和会话的隐私权,所有权和控制权。它允许您访问开放网络;因此,您可以与 Element 用户以外的人交流。并且它非常安全。\n\nElement 之所以可以做到这些,是因为它在 Matrix 上运行——开放,去中心化通讯的标准。\n\n通过让您选择由谁来托管您的会话,Element 让您掌控一切。在 Element 应用中,您可以选择不同的托管方式:\n\n1. 在由 Matrix 开发者托管的 matrix.org 公共服务器上获取免费账户,或从志愿者托管的上千个公共服务器中选择\n2. 在您自己的硬件上运行服务器,自托管您的会话\n3. 通过订阅 Element Matrix Services 托管平台,简单地在自定义服务器上注册账户\n\n为什么选择 Element?\n\n掌控您的数据:您来决定存放您的数据和消息的位置。拥有并控制它的是您,而不是挖掘您的数据或与第三方分享的巨型企业。\n\n开放通讯与协作:您可以与 Matrix 网络中的任何人聊天,不论他们使用 Element 还是其他 Matrix 应用,甚至/即使他们在使用不同的通讯系统,例如 Slack、IRC 或 XMPP。\n\n超级安全:支持真正的端到端加密(仅有会话中的人可以解密消息),还有能够验证会话参与方的设备的交叉签名。\n\n完善的通讯方式:消息,语音和视频通话,文件共享,屏幕共享和大量集成功能,机器人和挂件。建立房间与社区,保持联系并完成工作。\n\n随时随地:消息历史可在您的全部设备和 https://app.element.io 网页端之间完全同步,无论您在哪里,都可以保持联系。";
|
||||
"auth_accept_policies" = "请查看并接受此主页服务器的服务条款:";
|
||||
"room_replacement_information" = "这个房间已被替换,不再有效。";
|
||||
"settings_flair" = "在允许的地方显示个性徽章";
|
||||
@@ -1090,7 +1090,7 @@
|
||||
"more" = "更多";
|
||||
"switch" = "开关";
|
||||
"joined" = "已加入";
|
||||
"store_promotional_text" = "在开放网络上保护隐私的聊天和协作应用程序。分散权力让你掌控一切。没有数据挖掘,没有后门,也没有第三方访问。";
|
||||
"store_promotional_text" = "在开放网络上保护隐私的聊天和协作应用程序。去中心化让你掌控一切。没有数据挖掘,没有后门,也没有第三方访问。";
|
||||
"social_login_button_title_sign_up" = "使用 %@ 注册";
|
||||
"social_login_button_title_sign_in" = "使用 %@ 登录";
|
||||
"social_login_button_title_continue" = "使用 %@ 继续";
|
||||
@@ -2215,3 +2215,19 @@
|
||||
"authentication_forgot_password_input_message" = "%@将给你发一条验证链接";
|
||||
"authentication_forgot_password_input_title" = "输入你的电子邮件";
|
||||
"authentication_verify_email_waiting_button" = "重发电子邮件";
|
||||
"ignore_user" = "忽略用户";
|
||||
"authentication_qr_login_start_need_alternative" = "需要替代方法?";
|
||||
"authentication_qr_login_start_step4" = "选择“在此设备显示QR码”";
|
||||
"authentication_qr_login_start_display_qr" = "在此设备显示QR码";
|
||||
"user_sessions_overview_link_device" = "关联设备";
|
||||
"authentication_qr_login_display_title" = "关联设备";
|
||||
"authentication_qr_login_start_step3" = "选择“关联设备”";
|
||||
"authentication_qr_login_start_step2" = "前往设置->安全与隐私";
|
||||
"authentication_qr_login_start_step1" = "打开你的其他设备上的Element";
|
||||
"authentication_qr_login_start_subtitle" = "用此设备的相机扫描显示在你的其他设备上的QR码:";
|
||||
"authentication_qr_login_start_title" = "扫描QR码";
|
||||
"authentication_choose_password_signout_all_devices" = "登出全部设备";
|
||||
"authentication_login_with_qr" = "用QR码登录";
|
||||
"onboarding_congratulations_home_button" = "带我到主页";
|
||||
"onboarding_use_case_message" = "我们将帮助你连接";
|
||||
"invite_to" = "邀请到%@";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Permissions usage explanations
|
||||
"NSCameraUsageDescription" = "相機權限會用來拍攝照片與影片,以及進行視訊通話。";
|
||||
"NSPhotoLibraryUsageDescription" = "照片圖庫的權限會用來傳送照片與影片。";
|
||||
"NSMicrophoneUsageDescription" = "Element需要麥克風的權限來拍攝影片、照片以及進行通話。";
|
||||
"NSContactsUsageDescription" = "為了要顯示您的聯絡人中哪些人已在使用 Element 或 Matrix,我們將會傳送聯絡資訊內的電子郵件位址與電話給您的 Matrix 身份伺服器。New Vector 不會儲存這些資訊,也不會將這些資訊用於其他目的。請檢視應用程式設定的隱私權政策頁面來取得更多資訊。";
|
||||
"NSCameraUsageDescription" = "相機權限會用來拍攝照片、影片,與進行視訊通話。";
|
||||
"NSPhotoLibraryUsageDescription" = "允許讀取照片圖庫權限並用來傳送照片與影片。";
|
||||
"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來進行語音通話、視訊通話與錄製語音訊息。";
|
||||
"NSContactsUsageDescription" = "這將會分享給身份伺服器以便在 Matrix 尋找您的聯絡人。";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "當您分享您的位置給其他人時,Element 需要權限來顯示地圖。";
|
||||
"NSLocationWhenInUseUsageDescription" = "當您分享您的位置給其他人時,Element 需要權限來顯示地圖。";
|
||||
"NSFaceIDUsageDescription" = "已啟用 Face ID 來使用您的應用程式。";
|
||||
"NSCalendarsUsageDescription" = "檢視您已排定的會議。";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* New message from a specific person, not referencing a room */
|
||||
"MSG_FROM_USER" = "從 %@ 來的訊息";
|
||||
"MSG_FROM_USER" = "%@ 傳來的訊息";
|
||||
/* New message from a specific person in a named room */
|
||||
"MSG_FROM_USER_IN_ROOM" = "%@ 在 %@ 貼文";
|
||||
/* New message from a specific person, not referencing a room. Content included. */
|
||||
@@ -51,3 +51,81 @@
|
||||
"SINGLE_UNREAD" = "您收到了一則訊息";
|
||||
/* Message title for a specific person in a named room */
|
||||
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ 從 %@";
|
||||
|
||||
/* New message reply from a specific person in a named room. */
|
||||
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ 在 %@ 已回覆";
|
||||
|
||||
/* New message reply from a specific person, not referencing a room. */
|
||||
"REPLY_FROM_USER_TITLE" = "%@ 已回覆";
|
||||
/** General **/
|
||||
|
||||
"Notification" = "通知";
|
||||
|
||||
/** Key verification **/
|
||||
|
||||
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ 請求驗證";
|
||||
|
||||
/* Group call from user, CallKit caller name */
|
||||
"GROUP_CALL_FROM_USER" = "%@ (群組通話)";
|
||||
|
||||
/* A user added a Jitsi call to a room */
|
||||
"GROUP_CALL_STARTED" = "群組通話開始";
|
||||
|
||||
/* A user's membership has updated in an unknown way */
|
||||
"USER_MEMBERSHIP_UPDATED" = "%@ 更新了簡介";
|
||||
|
||||
/* A user has change their name to a new name which we don't know */
|
||||
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱";
|
||||
|
||||
/** Membership Updates **/
|
||||
|
||||
/* A user has change their name to a new name */
|
||||
"USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱為 %@";
|
||||
|
||||
/* A user has change their avatar */
|
||||
"USER_UPDATED_AVATAR" = "%@ 變更頭像";
|
||||
|
||||
/* A user has reacted to a message, but the reaction content is unknown */
|
||||
"GENERIC_REACTION_FROM_USER" = "%@ 送出一個反應";
|
||||
|
||||
/** Reactions **/
|
||||
|
||||
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
|
||||
"REACTION_FROM_USER" = "%@ 覺得 %@";
|
||||
|
||||
/* New message with hidden content due to PIN enabled */
|
||||
"MESSAGE_PROTECTED" = "新訊息";
|
||||
|
||||
/* New message indicator on a room */
|
||||
"MESSAGE_IN_X" = "在 %@ 的訊息";
|
||||
|
||||
/* New message indicator from a DM */
|
||||
"MESSAGE_FROM_X" = "來自 %@ 的訊息";
|
||||
|
||||
/** Notification messages **/
|
||||
|
||||
/* New message indicator on unknown room */
|
||||
"MESSAGE" = "訊息";
|
||||
|
||||
/* Sticker from a specific person, not referencing a room. */
|
||||
"STICKER_FROM_USER" = "%@ 戳你一下";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"LOCATION_FROM_USER" = "%@ 已分享他的位置";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"FILE_FROM_USER" = "%@ 傳送一個檔案 %@";
|
||||
|
||||
/* New voice message from a specific person, not referencing a room. */
|
||||
"VOICE_MESSAGE_FROM_USER" = "%@ 傳送一個語音訊息";
|
||||
|
||||
/* New audio message from a specific person, not referencing a room. */
|
||||
"AUDIO_FROM_USER" = "%@ 傳送一個音訊檔案 %@";
|
||||
|
||||
/* New video message from a specific person, not referencing a room. */
|
||||
"VIDEO_FROM_USER" = "%@ 傳送一個影片";
|
||||
|
||||
/** Media Messages **/
|
||||
|
||||
/* New image message from a specific person, not referencing a room. */
|
||||
"PICTURE_FROM_USER" = "%@ 傳送一張圖片";
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"bug_report_send" = "傳送";
|
||||
"room_event_action_resend" = "重新傳送";
|
||||
"room_event_action_view_source" = "檢視來源";
|
||||
"room_event_action_permalink" = "永久連結";
|
||||
"room_event_action_permalink" = "複製訊息永久連結";
|
||||
"room_event_action_quote" = "引用";
|
||||
"room_participants_online" = "線上";
|
||||
"room_details_favourite_tag" = "我的最愛";
|
||||
@@ -164,9 +164,9 @@
|
||||
"e2e_room_key_request_title" = "加密金鑰請求";
|
||||
"e2e_room_key_request_share_without_verifying" = "不驗證就分享";
|
||||
"e2e_room_key_request_ignore_request" = "忽略請求";
|
||||
"auth_reset_password_message" = "要重設您的密碼,輸入連結到您的帳號的電子郵件地址:";
|
||||
"auth_reset_password_message" = "為了重設密碼,請輸入您的電子郵件地址:";
|
||||
"auth_reset_password_email_validation_message" = "電子郵件已傳送至 %@。您必須跟隨其中包含了連結,點按下面的連結。";
|
||||
"auth_reset_password_success_message" = "您的密碼已重設。\n\n您已在所有工作階段上登出,並且不會再收到推送通知。要重新啟用通知,再次於每個裝置上登入。";
|
||||
"auth_reset_password_success_message" = "您的密碼已重設。\n\n您已登出所有工作階段,並且不會再收到推送通知。如要重新啟用通知,請於每個裝置重新登入。";
|
||||
"auth_add_email_and_phone_warning" = "直到 API 存在之前,尚不支援同時使用電子郵件地址和電話號碼註冊,因此只有電話號碼會被採用,但您可以在基本資料中新增電子郵件地址。";
|
||||
// Chat creation
|
||||
"room_creation_title" = "新的聊天";
|
||||
@@ -437,7 +437,7 @@
|
||||
"group_participants_remove_prompt_title" = "確認";
|
||||
"group_participants_remove_prompt_msg" = "您確定要從該群組移除 %@ 嗎?";
|
||||
"group_participants_invite_prompt_title" = "確認";
|
||||
"group_participants_invite_prompt_msg" = "您確定要邀請 %@ 加入此群組嗎?";
|
||||
"group_participants_invite_prompt_msg" = "您確定要邀請 %@ 加入此群組嗎?";
|
||||
"group_participants_invite_another_user" = "使用使用者 ID 或名稱搜尋/邀請使用者";
|
||||
"group_participants_invite_malformed_id_title" = "邀請錯誤";
|
||||
"group_participants_invite_malformed_id" = "ID 格式錯誤。一個 Matrix ID 看起來應該像 '@localpart:domain'";
|
||||
@@ -501,7 +501,7 @@
|
||||
"bug_report_logs_description" = "為了診斷問題,此用戶端的記錄檔將會隨此錯誤報告送出。 如果您只想傳送上面的文字,請取消:";
|
||||
"widget_integration_missing_room_id" = "在請求中遺失 room_id 。";
|
||||
"widget_integration_missing_user_id" = "在請求中遺失 user_id 。";
|
||||
"widget_integration_room_not_visible" = "%@ 聊天室不可見。";
|
||||
"widget_integration_room_not_visible" = "%@ 聊天室為隱藏。";
|
||||
// Share extension
|
||||
"share_extension_auth_prompt" = "登入主應用程式以分享內容";
|
||||
"share_extension_failed_to_encrypt" = "傳送失敗。 檢查主應用程式對此聊天室的加密設定";
|
||||
@@ -601,10 +601,10 @@
|
||||
"auth_softlogout_signed_out" = "你已登出";
|
||||
"auth_autodiscover_invalid_response" = "無效的自家伺服器發現回應";
|
||||
"auth_accept_policies" = "請查看並接受此自家伺服器的策略:";
|
||||
"auth_reset_password_error_is_required" = "未配置身份伺服器:在伺服器選項中添加一個以便日後重置密碼。";
|
||||
"auth_forgot_password_error_no_configured_identity_server" = "未配置身份伺服器:添加一台以便重置密碼。";
|
||||
"auth_phone_is_required" = "沒有配置身份伺服器,因此您無法添加電話號碼以便將來重設密碼。";
|
||||
"auth_email_is_required" = "沒有配置身份伺服器,因此您無法添加電子郵件地址以便將來重設密碼。";
|
||||
"auth_reset_password_error_is_required" = "未設置身份伺服器:在伺服器選項中新增一個來重置密碼。";
|
||||
"auth_forgot_password_error_no_configured_identity_server" = "未設置身份伺服器:新增設置來重置密碼。";
|
||||
"auth_phone_is_required" = "沒有設置身份伺服器,因此無法新增電話號碼以便將來重設密碼。";
|
||||
"auth_email_is_required" = "沒有設置身份伺服器,因此無法新增電子郵件地址以便將來重設密碼。";
|
||||
"auth_add_email_phone_message_2" = "設置一個電子郵件以便日後恢復帳戶和使以後可以由認識您的人發現你。";
|
||||
"auth_add_email_message_2" = "設置一個電子郵件以便日後恢復帳戶和使以後可以由認識您的人發現你。";
|
||||
"auth_add_phone_message_2" = "設置一個電話號碼,以後可以由認識您的人發現你。";
|
||||
@@ -687,7 +687,7 @@
|
||||
// MARK: - Call Transfer
|
||||
"call_transfer_title" = "傳輸";
|
||||
"room_info_list_section_other" = "其他";
|
||||
"create_room_placeholder_topic" = "主題";
|
||||
"create_room_placeholder_topic" = "聊天室主題為何?";
|
||||
"create_room_placeholder_name" = "名稱";
|
||||
"biometrics_cant_unlocked_alert_message_retry" = "重試";
|
||||
"pin_protection_reset_alert_action_reset" = "重設";
|
||||
@@ -944,7 +944,7 @@
|
||||
"room_event_encryption_info_block" = "黑名單";
|
||||
"room_event_encryption_info_unblock" = "解除黑名單";
|
||||
"room_event_encryption_verify_title" = "驗證裝置\n\n";
|
||||
"room_event_encryption_verify_message" = "若要檢查這個裝置是可被信任的,請透過其他方法聯絡所有者(例如面對面或是在電話中),並詢問在其使用者設定中以下金鑰是否是一致的:\n\n\n\t裝置名稱:%@\n\t裝置 ID:%@\n\t裝置金鑰:%@\n\n若相同,請點選下面的「驗證確認」按鈕。如果不相同,表示有人從中攔截這個裝置,您可能要點選「黑名單」按鈕。\n\n未來驗證手續會更加簡單,若有不便敬請見諒。";
|
||||
"room_event_encryption_verify_message" = "若要檢查這個裝置是可被信任的,請透過其他方法聯絡所有者(例如面對面或是在電話中),並詢問在其使用者設定中以下金鑰是否是一致的:\n\n\t裝置名稱:%@\n\t裝置 ID:%@\n\t裝置金鑰:%@\n\n若相同,請點選下面的「驗證確認」按鈕。如果不相同,表示有人從中攔截這個裝置,您可能要點選「黑名單」按鈕。\n\n未來驗證手續會更加簡單,若有不便敬請見諒。";
|
||||
"room_event_encryption_verify_ok" = "驗證確認";
|
||||
// Account
|
||||
"account_save_changes" = "儲存修改";
|
||||
@@ -976,7 +976,7 @@
|
||||
"microphone_access_not_granted_for_call" = "電話需要使用麥克風權限,但是 %@ 沒有存取權限";
|
||||
"local_contacts_access_not_granted" = "從本機的聯絡資訊探索使用者,需要存取聯絡資訊的權限,但是 %@ 沒有存取權限";
|
||||
"local_contacts_access_discovery_warning_title" = "使用者探索";
|
||||
"local_contacts_access_discovery_warning" = "%@ 要從您的聯絡資訊上傳電子郵件位址跟電話號碼來探索使用者";
|
||||
"local_contacts_access_discovery_warning" = "為了找查您的通訊錄中是否已有 Matrix 帳號的聯絡人,%@ 將會從您的通訊錄中傳送電子郵件與電話號碼到您所選擇的身分伺服器中。個人資料會在傳送前雜湊內容(無法解讀的內容),若需要更多細節,請查閱您的身分伺服器的隱私權政策。";
|
||||
// Country picker
|
||||
"country_picker_title" = "選擇國家";
|
||||
// Language picker
|
||||
@@ -997,8 +997,8 @@
|
||||
"notice_display_name_set" = "%@ 設定了自己的顯示名稱為 %@";
|
||||
"notice_display_name_changed_from" = "%@ 將自己的顯示名稱從 %@ 改為 %@";
|
||||
"notice_display_name_removed" = "%@ 移除了自己的顯示名稱";
|
||||
"notice_topic_changed" = "%@ 已經變更主題為:%@";
|
||||
"notice_room_name_changed" = "%@ 將房間名稱變更為 %@";
|
||||
"notice_topic_changed" = "%@ 已變更主題為 %@。";
|
||||
"notice_room_name_changed" = "%@ 將聊天室名稱變更為 %@。";
|
||||
"notice_placed_voice_call" = "%@ 開始了語音通話";
|
||||
"notice_placed_video_call" = "%@ 開始了視訊通話";
|
||||
"notice_answered_video_call" = "%@ 接聽了通話";
|
||||
@@ -1099,7 +1099,7 @@
|
||||
"notice_event_redacted_by" = " 由 %@";
|
||||
"notice_event_redacted_reason" = " [理由:%@]";
|
||||
"notice_profile_change_redacted" = "%@ 已更新他的個人檔案 %@";
|
||||
"notice_room_created" = "%@ 創建了該聊天室";
|
||||
"notice_room_created" = "%@ 已建立與設定此聊天室。";
|
||||
"notice_room_join_rule" = "加入規則: %@";
|
||||
"notice_room_power_level_intro" = "聊天室成員們的權限级别是:";
|
||||
"notice_event_redacted" = "<撤回%@>";
|
||||
@@ -1122,7 +1122,7 @@
|
||||
"device_details_name" = "名稱\n";
|
||||
"device_details_identifier" = "裝置代碼\n";
|
||||
"device_details_last_seen" = "上次使用\n";
|
||||
"device_details_rename_prompt_message" = "裝置名稱:";
|
||||
"device_details_rename_prompt_message" = "公開名稱可見於與您聊天的對象";
|
||||
"login_error_resource_limit_exceeded_title" = "超過資源限制";
|
||||
"login_error_resource_limit_exceeded_message_default" = "此家伺服器已經超過其中一項資源限制。";
|
||||
"login_error_resource_limit_exceeded_message_monthly_active_user" = "此家伺服器已經達到其每月活躍使用者限制。";
|
||||
@@ -1142,3 +1142,131 @@
|
||||
"stop" = "停止";
|
||||
"joining" = "正在加入";
|
||||
"enable" = "啓用";
|
||||
"service_terms_modal_policy_checkbox_accessibility_hint" = "確認接受 %@";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_terms_message" = "請閱讀 %@ 的條款與政策";
|
||||
"authentication_terms_title" = "隱私權政策";
|
||||
"authentication_verify_msisdn_invalid_phone_number" = "無效的電話號碼或格式";
|
||||
"authentication_verify_msisdn_waiting_button" = "重新傳送驗證碼";
|
||||
/* The placeholder will show the phone number that was entered. */
|
||||
"authentication_verify_msisdn_waiting_message" = "驗證碼已傳送到 %@";
|
||||
"authentication_verify_msisdn_waiting_title" = "確認與驗證您的電話號碼";
|
||||
"authentication_verify_msisdn_otp_text_field_placeholder" = "驗證碼";
|
||||
"authentication_verify_msisdn_text_field_placeholder" = "電話號碼";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_verify_msisdn_input_message" = "%@ 需要驗證您的帳號";
|
||||
"authentication_verify_msisdn_input_title" = "輸入您的電話號碼";
|
||||
"authentication_choose_password_not_verified_message" = "檢查您的郵件收件夾";
|
||||
"authentication_choose_password_not_verified_title" = "郵件信箱尚未確認與驗證";
|
||||
"authentication_choose_password_submit_button" = "重新設定密碼";
|
||||
"authentication_choose_password_signout_all_devices" = "登出所有裝置";
|
||||
"authentication_choose_password_text_field_placeholder" = "新密碼";
|
||||
"authentication_choose_password_input_message" = "確認至少 8 字元或更多";
|
||||
"authentication_choose_password_input_title" = "選擇新密碼";
|
||||
"authentication_forgot_password_waiting_button" = "重新寄送";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_forgot_password_waiting_message" = "依照指示已寄送到 %@";
|
||||
"authentication_forgot_password_waiting_title" = "檢查或確認您的郵件信箱。";
|
||||
"authentication_forgot_password_text_field_placeholder" = "郵件信箱";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_forgot_password_input_message" = "%@ 將會傳送驗證連結給您";
|
||||
"authentication_forgot_password_input_title" = "輸入您的電子郵件信箱";
|
||||
"authentication_verify_email_waiting_button" = "重新寄送";
|
||||
"authentication_verify_email_waiting_hint" = "還未收到信件?";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_verify_email_waiting_message" = "依照指示已寄送到 %@";
|
||||
"authentication_verify_email_waiting_title" = "驗證您的郵件信箱。";
|
||||
"authentication_verify_email_text_field_placeholder" = "郵件信箱";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_verify_email_input_message" = "%@ 需要驗證您的帳號";
|
||||
"authentication_verify_email_input_title" = "輸入您的電子郵件信箱";
|
||||
"authentication_cancel_flow_confirmation_message" = "您的帳號尚未建立完成,確定要退出註冊過程?";
|
||||
"authentication_server_selection_generic_error" = "這個 URL 無法找到伺服器,請確認它是否正確。";
|
||||
"authentication_server_selection_server_url" = "主伺服器 URL";
|
||||
"authentication_server_selection_register_message" = "屬於您伺服器的位置為何?是存放您所有資訊的主要地方";
|
||||
"authentication_server_selection_register_title" = "選擇您的主伺服器";
|
||||
"authentication_server_selection_login_message" = "屬於您伺服器的位置為何?";
|
||||
"authentication_server_selection_login_title" = "連線到主伺服器";
|
||||
"authentication_login_with_qr" = "透過 QR code 登入";
|
||||
"authentication_server_info_title_login" = "您的對話訊息將會被保存的位置";
|
||||
"authentication_login_forgot_password" = "忘記密碼";
|
||||
"authentication_login_username" = "使用者帳號 / 電子郵件 / 電話號碼";
|
||||
"authentication_login_title" = "歡迎回來!";
|
||||
"authentication_server_info_title" = "您的對話訊息將會被保存的位置";
|
||||
"authentication_registration_password_footer" = "至少 8 字元或更多";
|
||||
/* The placeholder will show the full Matrix ID that has been entered. */
|
||||
"authentication_registration_username_footer_available" = "其他人可以找到您 %@";
|
||||
"authentication_registration_username_footer" = "選定後就無法在之後變更修改";
|
||||
"authentication_registration_username" = "使用者帳號";
|
||||
|
||||
// MARK: Authentication
|
||||
"authentication_registration_title" = "建立您的帳號";
|
||||
"onboarding_celebration_button" = "開始吧";
|
||||
"onboarding_celebration_message" = "隨時都可更新您的個人簡介";
|
||||
"onboarding_celebration_title" = "看起來不錯喔!";
|
||||
"onboarding_avatar_accessibility_label" = "簡介圖片";
|
||||
"onboarding_avatar_message" = "是時候將名字與臉孔聯繫在一起了";
|
||||
"onboarding_avatar_title" = "新增簡介圖片";
|
||||
"onboarding_display_name_max_length" = "您的顯示名稱必須小於 256 字元";
|
||||
"onboarding_display_name_hint" = "您可以之後再變更";
|
||||
"onboarding_display_name_placeholder" = "顯示名稱";
|
||||
"onboarding_display_name_message" = "當您傳送訊息這將會被顯示。";
|
||||
"onboarding_display_name_title" = "選擇顯示名稱";
|
||||
"onboarding_personalization_skip" = "略過此步驟";
|
||||
"onboarding_personalization_save" = "儲存並繼續";
|
||||
"onboarding_congratulations_home_button" = "帶我回家";
|
||||
"onboarding_congratulations_personalize_button" = "個人簡介";
|
||||
/* The placeholder string contains the user's matrix ID */
|
||||
"onboarding_congratulations_message" = "您的帳號 %@ 已建立了";
|
||||
"onboarding_congratulations_title" = "恭喜!";
|
||||
"onboarding_use_case_existing_server_button" = "連線到伺服器";
|
||||
"onboarding_use_case_existing_server_message" = "尋找加入一個存在的伺服器?";
|
||||
"onboarding_use_case_skip_button" = "略過這個問題";
|
||||
/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */
|
||||
"onboarding_use_case_not_sure_yet" = "還不確定? %@";
|
||||
"onboarding_use_case_community_messaging" = "社群";
|
||||
"onboarding_use_case_work_messaging" = "團隊";
|
||||
"onboarding_use_case_personal_messaging" = "朋友與家人";
|
||||
"onboarding_use_case_message" = "我們會協助您連線";
|
||||
"onboarding_use_case_title" = "您最常聊天的對象是誰?";
|
||||
"onboarding_splash_page_4_message" = "Element 也非常適合工作場域使用。它受到世界上最安全的組織所信任。";
|
||||
"onboarding_splash_page_4_title_no_pun" = "與您的團隊通訊。";
|
||||
"onboarding_splash_page_3_message" = "端到端加密與不需要電話號碼。無廣告或資料蒐集。";
|
||||
"onboarding_splash_page_1_message" = "為您提供與在家中面對面交談時相同的隱私等級、安全且獨立的通訊。";
|
||||
"onboarding_splash_page_3_title" = "安全通訊中。";
|
||||
"onboarding_splash_page_2_message" = "選擇您的對話將保存在哪裡,一切將由您獨立掌控。透過 Matrix 連線。";
|
||||
"onboarding_splash_page_2_title" = "一切都在您的掌控之中。";
|
||||
"onboarding_splash_page_1_title" = "掌握您的對話。";
|
||||
"onboarding_splash_login_button_title" = "我已經有帳號了";
|
||||
|
||||
// MARK: Onboarding
|
||||
"onboarding_splash_register_button_title" = "建立帳號";
|
||||
"accessibility_button_label" = "按鈕";
|
||||
"callbar_only_single_active_group" = "點擊加入群組通話 (%@)";
|
||||
"callbar_only_multiple_paused" = "%@ 通暫停通話";
|
||||
"callbar_only_single_paused" = "暫停通話";
|
||||
"callbar_active_and_multiple_paused" = "1 個正在進行通話 (%@) · %@ 個暫停的通話";
|
||||
"callbar_active_and_single_paused" = "1 個正在進行通話 (%@) · 1 個暫停的通話";
|
||||
|
||||
// Call Bar
|
||||
"callbar_only_single_active" = "點擊返回通話 (%@)";
|
||||
"saving" = "儲存中";
|
||||
|
||||
// Activities
|
||||
"loading" = "讀取中";
|
||||
"invite_to" = "邀請到 %@";
|
||||
"confirm" = "確認";
|
||||
"edit" = "編輯";
|
||||
"suggest" = "建議";
|
||||
"add" = "新增";
|
||||
"existing" = "已存在";
|
||||
"new_word" = "新增";
|
||||
|
||||
// GDPR
|
||||
"gdpr_consent_not_given_alert_message" = "如要繼續使用 %@ 服務伺服器,您必須檢視與同意條款與條件。";
|
||||
"settings_callkit_info" = "在鎖定畫面接聽來電。顯示您的 %@ 通話於系統通話紀錄。若啟用 iCloud,通話紀錄將被分享給 Apple。";
|
||||
"room_many_users_are_typing" = "%@, %@ 與其他人正在輸入 …";
|
||||
/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. Note the > at the start indicates "more than 20 results". */
|
||||
"directory_search_results_more_than" = ">%1$tu 個搜尋結果關於 %2$@";
|
||||
/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. */
|
||||
"directory_search_results" = "%1$tu 個搜尋結果關於 %2$@";
|
||||
|
||||
@@ -46,4 +46,14 @@ extension MXEvent {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
var isTimelinePollEvent: Bool {
|
||||
switch eventType {
|
||||
case .pollStart, .pollEnd:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2022 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 MatrixSDK
|
||||
|
||||
extension MXRoom {
|
||||
|
||||
func stopUncompletedVoiceBroadcastIfNeeded() {
|
||||
// Detection of a potential uncompleted VoiceBroadcast
|
||||
// Check whether a VoiceBroadcast is in progress on the current session for this room whereas no VoiceBroadcast Service is available.
|
||||
self.lastVoiceBroadcastStateEvent { event in
|
||||
guard let event = event,
|
||||
event.stateKey == self.mxSession.myUserId,
|
||||
let eventDeviceId = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyDeviceId] as? String,
|
||||
eventDeviceId == self.mxSession.myDeviceId,
|
||||
let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content),
|
||||
voiceBroadcastInfo.state != VoiceBroadcastInfoState.stopped.rawValue,
|
||||
self.mxSession.voiceBroadcastService == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.mxSession.getOrCreateVoiceBroadcastService(for: self) { service in
|
||||
guard let service = service else {
|
||||
return
|
||||
}
|
||||
|
||||
service.stopVoiceBroadcast(lastChunkSequence: 0,
|
||||
voiceBroadcastId: voiceBroadcastInfo.voiceBroadcastId ?? event.eventId) { response in
|
||||
MXLog.debug("[MXRoom] stopUncompletedVoiceBroadcastIfNeeded stopVoiceBroadcast with response : \(response)")
|
||||
self.mxSession.tearDownVoiceBroadcastService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lastVoiceBroadcastStateEvent(completion: @escaping (MXEvent?) -> Void) {
|
||||
self.state { roomState in
|
||||
completion(roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright 2022 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.
|
||||
//
|
||||
|
||||
extension Sequence {
|
||||
func group<GroupID: Hashable>(by keyPath: KeyPath<Element, GroupID>) -> [GroupID: [Element]] {
|
||||
var result: [GroupID: [Element]] = .init()
|
||||
|
||||
for item in self {
|
||||
let groupId = item[keyPath: keyPath]
|
||||
result[groupId] = (result[groupId] ?? []) + [item]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -358,11 +358,16 @@ internal class Asset: NSObject {
|
||||
internal static let tabHome = ImageAsset(name: "tab_home")
|
||||
internal static let tabPeople = ImageAsset(name: "tab_people")
|
||||
internal static let tabRooms = ImageAsset(name: "tab_rooms")
|
||||
internal static let voiceBroadcastBackward30s = ImageAsset(name: "voice_broadcast_backward_30s")
|
||||
internal static let voiceBroadcastForward30s = ImageAsset(name: "voice_broadcast_forward_30s")
|
||||
internal static let voiceBroadcastLive = ImageAsset(name: "voice_broadcast_live")
|
||||
internal static let voiceBroadcastPause = ImageAsset(name: "voice_broadcast_pause")
|
||||
internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play")
|
||||
internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record")
|
||||
internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause")
|
||||
internal static let voiceBroadcastSliderMaxTrack = ImageAsset(name: "voice_broadcast_slider_max_track")
|
||||
internal static let voiceBroadcastSliderMinTrack = ImageAsset(name: "voice_broadcast_slider_min_track")
|
||||
internal static let voiceBroadcastSliderThumb = ImageAsset(name: "voice_broadcast_slider_thumb")
|
||||
internal static let voiceBroadcastSpinner = ImageAsset(name: "voice_broadcast_spinner")
|
||||
internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
|
||||
internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live")
|
||||
|
||||
@@ -4323,6 +4323,18 @@ public class VectorL10n: NSObject {
|
||||
public static var noticeVideoAttachment: String {
|
||||
return VectorL10n.tr("Vector", "notice_video_attachment")
|
||||
}
|
||||
/// %@ ended a voice broadcast.
|
||||
public static func noticeVoiceBroadcastEnded(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "notice_voice_broadcast_ended", p1)
|
||||
}
|
||||
/// You ended a voice broadcast.
|
||||
public static var noticeVoiceBroadcastEndedByYou: String {
|
||||
return VectorL10n.tr("Vector", "notice_voice_broadcast_ended_by_you")
|
||||
}
|
||||
/// Live broadcast
|
||||
public static var noticeVoiceBroadcastLive: String {
|
||||
return VectorL10n.tr("Vector", "notice_voice_broadcast_live")
|
||||
}
|
||||
/// Always notify
|
||||
public static var notificationSettingsAlwaysNotify: String {
|
||||
return VectorL10n.tr("Vector", "notification_settings_always_notify")
|
||||
@@ -4827,6 +4839,14 @@ public class VectorL10n: NSObject {
|
||||
public static var pollEditFormUpdateFailureTitle: String {
|
||||
return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title")
|
||||
}
|
||||
/// Due to decryption errors, some votes may not be counted
|
||||
public static var pollTimelineDecryptionError: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_decryption_error")
|
||||
}
|
||||
/// Ended the poll
|
||||
public static var pollTimelineEndedText: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_ended_text")
|
||||
}
|
||||
/// Please try again
|
||||
public static var pollTimelineNotClosedSubtitle: String {
|
||||
return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle")
|
||||
@@ -7567,7 +7587,7 @@ public class VectorL10n: NSObject {
|
||||
public static var settingsLabsEnableThreads: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enable_threads")
|
||||
}
|
||||
/// Voice broadcast (under active development)
|
||||
/// Voice broadcast
|
||||
public static var settingsLabsEnableVoiceBroadcast: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enable_voice_broadcast")
|
||||
}
|
||||
@@ -8727,7 +8747,7 @@ public class VectorL10n: NSObject {
|
||||
public static var userOtherSessionPermanentlyUnverifiedAdditionalInfo: String {
|
||||
return VectorL10n.tr("Vector", "user_other_session_permanently_unverified_additional_info")
|
||||
}
|
||||
/// Security recommendation
|
||||
/// Other sessions
|
||||
public static var userOtherSessionSecurityRecommendationTitle: String {
|
||||
return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title")
|
||||
}
|
||||
@@ -9195,6 +9215,14 @@ public class VectorL10n: NSObject {
|
||||
public static var voiceBroadcastUnauthorizedTitle: String {
|
||||
return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title")
|
||||
}
|
||||
/// You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message
|
||||
public static var voiceMessageBroadcastInProgressMessage: String {
|
||||
return VectorL10n.tr("Vector", "voice_message_broadcast_in_progress_message")
|
||||
}
|
||||
/// Can't start voice message
|
||||
public static var voiceMessageBroadcastInProgressTitle: String {
|
||||
return VectorL10n.tr("Vector", "voice_message_broadcast_in_progress_title")
|
||||
}
|
||||
/// Voice message
|
||||
public static var voiceMessageLockScreenPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "voice_message_lock_screen_placeholder")
|
||||
@@ -9311,10 +9339,18 @@ public class VectorL10n: NSObject {
|
||||
public static var wysiwygComposerFormatActionBold: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_bold")
|
||||
}
|
||||
/// Apply inline code format
|
||||
public static var wysiwygComposerFormatActionInlineCode: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code")
|
||||
}
|
||||
/// Apply italic format
|
||||
public static var wysiwygComposerFormatActionItalic: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_italic")
|
||||
}
|
||||
/// Apply link format
|
||||
public static var wysiwygComposerFormatActionLink: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_link")
|
||||
}
|
||||
/// Apply underline format
|
||||
public static var wysiwygComposerFormatActionStrikethrough: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough")
|
||||
@@ -9323,6 +9359,22 @@ public class VectorL10n: NSObject {
|
||||
public static var wysiwygComposerFormatActionUnderline: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline")
|
||||
}
|
||||
/// Create a link
|
||||
public static var wysiwygComposerLinkActionCreateTitle: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_create_title")
|
||||
}
|
||||
/// Edit link
|
||||
public static var wysiwygComposerLinkActionEditTitle: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_edit_title")
|
||||
}
|
||||
/// Link
|
||||
public static var wysiwygComposerLinkActionLink: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_link")
|
||||
}
|
||||
/// Text
|
||||
public static var wysiwygComposerLinkActionText: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_text")
|
||||
}
|
||||
/// Attachments
|
||||
public static var wysiwygComposerStartActionAttachments: String {
|
||||
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_attachments")
|
||||
|
||||
@@ -30,6 +30,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||
public private(set) var query: String?
|
||||
public private(set) var space: MXSpace?
|
||||
private var fetchersCreated: Bool = false
|
||||
private var uncompletedVoiceBroadcastCleaningDone: Bool = false
|
||||
|
||||
// MARK: - Fetchers
|
||||
|
||||
@@ -757,6 +758,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol {
|
||||
forSection: section,
|
||||
totalCountsChanged: totalCountsChanged) }
|
||||
} else {
|
||||
stopUncompletedVoiceBroadcastIfNeeded()
|
||||
multicastDelegate.invoke { $0.recentsListServiceDidChangeData?(self,
|
||||
totalCountsChanged: totalCountsChanged) }
|
||||
}
|
||||
@@ -784,6 +786,31 @@ extension RecentsListService: MXRoomListDataFetcherDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - VoiceBroadcast
|
||||
extension RecentsListService {
|
||||
|
||||
private func stopUncompletedVoiceBroadcastIfNeeded() {
|
||||
guard uncompletedVoiceBroadcastCleaningDone == false,
|
||||
let breadcrumbsFetcher = breadcrumbsRoomListDataFetcher else {
|
||||
return
|
||||
}
|
||||
// We limit for the moment the uncompleted voice broadcast cleaning to the breadcrumbs rooms list
|
||||
stopUncompletedVoiceBroadcastIfNeeded(for: breadcrumbsFetcher)
|
||||
uncompletedVoiceBroadcastCleaningDone = true
|
||||
}
|
||||
|
||||
private func stopUncompletedVoiceBroadcastIfNeeded(for fetcher: MXRoomListDataFetcher) {
|
||||
fetcher.data?.rooms.forEach({ roomSummary in
|
||||
guard let roomSummary = roomSummary as? MXRoomSummary,
|
||||
let room = roomSummary.room else {
|
||||
return
|
||||
}
|
||||
|
||||
room.stopUncompletedVoiceBroadcastIfNeeded()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FetcherTypes
|
||||
|
||||
private struct FetcherTypes: OptionSet {
|
||||
|
||||
@@ -229,4 +229,7 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol {
|
||||
multicastDelegate.invoke({ $0.recentsListServiceDidChangeData?(self, totalCountsChanged: true) })
|
||||
}
|
||||
|
||||
public func stopUncompletedVoiceBroadcastIfNeeded(for listData: MatrixSDK.MXRoomListData?) {
|
||||
// nothing here
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,10 +80,7 @@
|
||||
// Manage lastEventAttributedTextMessage optional property
|
||||
if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)])
|
||||
{
|
||||
// Force the default text color for the last message (cancel highlighted message color)
|
||||
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:roomCellData.lastEventAttributedTextMessage];
|
||||
[lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor range:NSMakeRange(0, lastEventDescription.length)];
|
||||
self.lastEventDescription.attributedText = lastEventDescription;
|
||||
self.lastEventDescription.attributedText = roomCellData.lastEventAttributedTextMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
#import "MXKImageView.h"
|
||||
#import "MXKRoomBubbleCellData.h"
|
||||
#import "UserIndicatorCancel.h"
|
||||
#import "VoiceBroadcastInfo.h"
|
||||
|
||||
@@ -1916,8 +1916,15 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
|
||||
MXRoomSummary *summary = room.summary;
|
||||
if (summary)
|
||||
{
|
||||
NSString *eventId = summary.lastMessage.eventId;
|
||||
if (!eventId)
|
||||
{
|
||||
MXLogFailure(@"[MXKAccount] onDateTimeFormatUpdate: Missing event id");
|
||||
continue;
|
||||
}
|
||||
|
||||
dispatch_group_enter(dispatchGroup);
|
||||
[summary.mxSession eventWithEventId:summary.lastMessage.eventId
|
||||
[summary.mxSession eventWithEventId:eventId
|
||||
inRoom:summary.roomId
|
||||
success:^(MXEvent *event) {
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ static NSString *const kMXAppGroupID = @"group.org.matrix";
|
||||
kMXEventTypeStringKeyVerificationDone,
|
||||
kMXEventTypeStringPollStart,
|
||||
kMXEventTypeStringPollStartMSC3381,
|
||||
kMXEventTypeStringPollEnd,
|
||||
kMXEventTypeStringPollEndMSC3381,
|
||||
kMXEventTypeStringBeaconInfo,
|
||||
kMXEventTypeStringBeaconInfoMSC3672
|
||||
].mutableCopy;
|
||||
@@ -181,6 +183,8 @@ static NSString *const kMXAppGroupID = @"group.org.matrix";
|
||||
kMXEventTypeStringKeyVerificationDone,
|
||||
kMXEventTypeStringPollStart,
|
||||
kMXEventTypeStringPollStartMSC3381,
|
||||
kMXEventTypeStringPollEnd,
|
||||
kMXEventTypeStringPollEndMSC3381,
|
||||
kMXEventTypeStringBeaconInfo,
|
||||
kMXEventTypeStringBeaconInfoMSC3672
|
||||
].mutableCopy;
|
||||
|
||||
@@ -2881,10 +2881,21 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (event.eventType == MXEventTypePollStart) {
|
||||
if (event.isTimelinePollEvent) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Specific case for voice broadcast event
|
||||
if (event.eventType == MXEventTypeCustom &&
|
||||
[event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
|
||||
|
||||
// Ensures that we only support reactions for a start event
|
||||
VoiceBroadcastInfo* voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: event.content];
|
||||
if ([VoiceBroadcastInfo isStartedFor: voiceBroadcastInfo.state]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL isRoomMessage = (event.eventType == MXEventTypeRoomMessage);
|
||||
|
||||
if (!isRoomMessage) {
|
||||
|
||||
@@ -1606,6 +1606,23 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MXEventTypePollEnd:
|
||||
{
|
||||
if (event.isEditEvent)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
MXEvent* pollStartedEvent = [self->mxSession.store eventWithEventId:event.relatesTo.eventId inRoom:event.roomId];
|
||||
|
||||
if (pollStartedEvent) {
|
||||
displayText = [MXEventContentPollStart modelFromJSON:pollStartedEvent.content].question;
|
||||
} else {
|
||||
displayText = [VectorL10n pollTimelineEndedText];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MXEventTypePollStart:
|
||||
{
|
||||
if (event.isEditEvent)
|
||||
@@ -1983,8 +2000,8 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
}
|
||||
|
||||
// Replace <mx-reply><blockquote><a href=\"__permalink__\">In reply to</a>
|
||||
// By <mx-reply><blockquote><a href=\"#\">['In reply to' from resources]</a>
|
||||
// To disable the link and to localize the "In reply to" string
|
||||
// By <mx-reply><blockquote><a href=\"__permalink__\">['In reply to' from resources]</a>
|
||||
// To localize the "In reply to" string
|
||||
// This link is the first <a> HTML node of the html string
|
||||
|
||||
if (inReplyToTextRange.location != NSNotFound)
|
||||
@@ -1992,11 +2009,6 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=(?:'|\")(.*?)(?:'|\")>(
|
||||
html = [html stringByReplacingCharactersInRange:inReplyToTextRange withString:[VectorL10n noticeInReplyTo]];
|
||||
}
|
||||
|
||||
if (inReplyToLinkRange.location != NSNotFound)
|
||||
{
|
||||
html = [html stringByReplacingCharactersInRange:inReplyToLinkRange withString:@"#"];
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
break;
|
||||
}
|
||||
case MXEventTypePollStart:
|
||||
case MXEventTypePollEnd:
|
||||
{
|
||||
self.tag = RoomBubbleCellDataTagPoll;
|
||||
self.collapsable = NO;
|
||||
@@ -296,6 +297,14 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
[self updateBeaconInfoSummaryWithId:eventId andEvent:event];
|
||||
}
|
||||
|
||||
// Handle here the case where an audio chunk of a voice broadcast have been decrypted with delay
|
||||
// We take the opportunity of this update to disable the display of this chunk in the room timeline
|
||||
if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType]) {
|
||||
self.tag = RoomBubbleCellDataTagVoiceBroadcastNoDisplay;
|
||||
self.collapsable = NO;
|
||||
self.collapsed = NO;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -626,9 +635,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
{
|
||||
__block NSInteger firstVisibleComponentIndex = NSNotFound;
|
||||
|
||||
BOOL isPoll = (self.events.firstObject.eventType == MXEventTypePollStart);
|
||||
MXEvent *firstEvent = self.events.firstObject;
|
||||
BOOL isPoll = firstEvent.isTimelinePollEvent;
|
||||
BOOL isVoiceBroadcast = (firstEvent.eventType == MXEventTypeCustom && [firstEvent.type isEqualToString: VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]);
|
||||
|
||||
if ((isPoll || self.attachment) && self.bubbleComponents.count)
|
||||
if ((isPoll || self.attachment || isVoiceBroadcast) && self.bubbleComponents.count)
|
||||
{
|
||||
firstVisibleComponentIndex = 0;
|
||||
}
|
||||
@@ -1183,6 +1194,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
shouldAddEvent = NO;
|
||||
break;
|
||||
case MXEventTypePollStart:
|
||||
case MXEventTypePollEnd:
|
||||
shouldAddEvent = NO;
|
||||
break;
|
||||
case MXEventTypeBeaconInfo:
|
||||
shouldAddEvent = NO;
|
||||
break;
|
||||
case MXEventTypeCustom:
|
||||
|
||||
@@ -138,9 +138,9 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
// Add `roomViewController` to the NavigationRouter, only if it has been explicitly set as parameter
|
||||
if let navigationRouter = self.parameters.navigationRouter {
|
||||
if navigationRouter.modules.isEmpty == false {
|
||||
navigationRouter.push(self.roomViewController, animated: true, popCompletion: nil)
|
||||
navigationRouter.push(self, animated: true, popCompletion: nil)
|
||||
} else {
|
||||
navigationRouter.setRootModule(self.roomViewController, popCompletion: nil)
|
||||
navigationRouter.setRootModule(self, popCompletion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
@class ThreadsCoordinatorBridgePresenter;
|
||||
@class LiveLocationSharingBannerView;
|
||||
@class VoiceBroadcastService;
|
||||
@class ComposerLinkActionBridgePresenter;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -122,6 +123,8 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
|
||||
|
||||
@property (nonatomic) CGFloat wysiwygTranslation;
|
||||
|
||||
@property (nonatomic, strong, nullable) ComposerLinkActionBridgePresenter *composerLinkActionBridgePresenter;
|
||||
|
||||
|
||||
/**
|
||||
Retrieve the live data source in cases where the timeline is not live.
|
||||
|
||||
@@ -1078,6 +1078,9 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
|
||||
// Set room title view
|
||||
[self refreshRoomTitle];
|
||||
|
||||
// Stop any pending voice broadcast if needed
|
||||
[self stopUncompletedVoiceBroadcastIfNeeded];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2470,6 +2473,9 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevents listening a VB when recording a new one
|
||||
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
|
||||
|
||||
// Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room
|
||||
[session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
|
||||
if (voiceBroadcastService) {
|
||||
@@ -3999,7 +4005,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
if (!isJitsiCallEvent && !selectedEvent.isTimelinePollEvent &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
|
||||
@@ -4020,7 +4026,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}
|
||||
|
||||
if (selectedEvent.sentState == MXEventSentStateSent &&
|
||||
selectedEvent.eventType != MXEventTypePollStart &&
|
||||
!selectedEvent.isTimelinePollEvent &&
|
||||
// Forwarding of live-location shares still to be implemented
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
@@ -4036,7 +4042,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && BWIBuildSettings.shared.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
if (!isJitsiCallEvent && BWIBuildSettings.shared.messageDetailsAllowShare && !selectedEvent.isTimelinePollEvent &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
||||
@@ -7185,6 +7191,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
case MXEventTypeKeyVerificationDone:
|
||||
case MXEventTypeKeyVerificationCancel:
|
||||
case MXEventTypePollStart:
|
||||
case MXEventTypePollEnd:
|
||||
case MXEventTypeBeaconInfo:
|
||||
result = NO;
|
||||
break;
|
||||
@@ -7942,6 +7949,20 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)voiceMessageControllerDidRequestRecording:(VoiceMessageController *)voiceMessageController
|
||||
{
|
||||
MXSession* session = self.roomDataSource.mxSession;
|
||||
// Check whether the user is not already broadcasting here or in another room
|
||||
if (session.voiceBroadcastService)
|
||||
{
|
||||
[self showAlertWithTitle:[VectorL10n voiceMessageBroadcastInProgressTitle] message:[VectorL10n voiceMessageBroadcastInProgressMessage]];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController
|
||||
didRequestSendForFileAtURL:(NSURL *)url
|
||||
duration:(NSUInteger)duration
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WysiwygComposer
|
||||
|
||||
extension RoomViewController {
|
||||
// MARK: - Override
|
||||
@@ -243,6 +244,13 @@ extension RoomViewController {
|
||||
roomInputToolbarContainer.superview?.isHidden = isHidden
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didSendLinkAction(_ linkAction: LinkActionWrapper) {
|
||||
let presenter = ComposerLinkActionBridgePresenter(linkAction: linkAction)
|
||||
presenter.delegate = self
|
||||
composerLinkActionBridgePresenter = presenter
|
||||
presenter.present(from: self, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
@@ -281,3 +289,37 @@ private extension RoomViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomViewController: ComposerLinkActionBridgePresenterDelegate {
|
||||
func didRequestLinkOperation(_ linkOperation: WysiwygLinkOperation) {
|
||||
dismissPresenter { [weak self] in
|
||||
self?.wysiwygInputToolbar?.performLinkOperation(linkOperation)
|
||||
}
|
||||
}
|
||||
|
||||
func didDismissInteractively() {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func didCancel() {
|
||||
dismissPresenter(completion: nil)
|
||||
}
|
||||
|
||||
private func dismissPresenter(completion: (() -> Void)?) {
|
||||
self.composerLinkActionBridgePresenter?.dismiss(animated: true) { [weak self] in
|
||||
completion?()
|
||||
self?.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanup() {
|
||||
composerLinkActionBridgePresenter = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VoiceBroadcast
|
||||
extension RoomViewController {
|
||||
@objc func stopUncompletedVoiceBroadcastIfNeeded() {
|
||||
self.roomDataSource?.room.stopUncompletedVoiceBroadcastIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ class PollPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCell
|
||||
override func render(_ cellData: MXKCellData!) {
|
||||
super.render(cellData)
|
||||
|
||||
guard let contentView = roomCellContentView?.innerContentView,
|
||||
guard
|
||||
let contentView = roomCellContentView?.innerContentView,
|
||||
let bubbleData = cellData as? RoomBubbleCellData,
|
||||
let event = bubbleData.events.last,
|
||||
event.eventType == __MXEventType.pollStart,
|
||||
let controller = TimelinePollProvider.shared.buildTimelinePollVCForEvent(event) else {
|
||||
event.isTimelinePollEvent,
|
||||
let controller = TimelinePollProvider.shared.buildTimelinePollVCForEvent(event)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ class VoiceBroadcastPlaybackPlainCell: SizableBaseRoomCell, RoomCellReactionsDis
|
||||
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
|
||||
}
|
||||
|
||||
// The normal flow for long press on cell content views doesn't work for bubbles without attributed strings
|
||||
override func onLongPressGesture(_ longPressGestureRecognizer: UILongPressGestureRecognizer!) {
|
||||
guard let event = self.event else {
|
||||
return
|
||||
}
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellLongPressOnEvent, userInfo: [kMXKRoomBubbleCellEventKey: event])
|
||||
}
|
||||
}
|
||||
|
||||
extension VoiceBroadcastPlaybackPlainCell: RoomCellThreadSummaryDisplayable {}
|
||||
|
||||
@@ -54,6 +54,14 @@ class VoiceBroadcastRecorderPlainCell: SizableBaseRoomCell, RoomCellReactionsDis
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
|
||||
}
|
||||
|
||||
// The normal flow for long press on cell content views doesn't work for bubbles without attributed strings
|
||||
override func onLongPressGesture(_ longPressGestureRecognizer: UILongPressGestureRecognizer!) {
|
||||
guard let event = self.event else {
|
||||
return
|
||||
}
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellLongPressOnEvent, userInfo: [kMXKRoomBubbleCellEventKey: event])
|
||||
}
|
||||
|
||||
func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
|
||||
|
||||
self.voiceBroadcastView?.removeFromSuperview()
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
@class RoomActionsBar;
|
||||
@class RoomInputToolbarView;
|
||||
@class LinkActionWrapper;
|
||||
|
||||
/**
|
||||
Destination of the message in the composer
|
||||
@@ -77,6 +78,8 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
|
||||
|
||||
- (void)didChangeMaximisedState: (BOOL) isMaximised;
|
||||
|
||||
- (void)didSendLinkAction: (LinkActionWrapper *)linkAction;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,11 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
private var voiceMessageBottomConstraint: NSLayoutConstraint?
|
||||
private var hostingViewController: VectorHostingController!
|
||||
private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent)
|
||||
private var wysiwygViewModel = WysiwygComposerViewModel(
|
||||
textColor: ThemeService.shared().theme.colors.primaryContent,
|
||||
linkColor: ThemeService.shared().theme.colors.accent,
|
||||
codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor
|
||||
)
|
||||
private var viewModel: ComposerViewModelProtocol!
|
||||
|
||||
private var isLandscapePhone: Bool {
|
||||
@@ -212,6 +216,13 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
||||
wysiwygViewModel.maximised = false
|
||||
}
|
||||
|
||||
func performLinkOperation(_ linkOperation: WysiwygLinkOperation) {
|
||||
if let selectionToRestore = viewModel.selectionToRestore {
|
||||
wysiwygViewModel.select(range: selectionToRestore)
|
||||
}
|
||||
wysiwygViewModel.applyLinkOperation(linkOperation)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Notification) {
|
||||
@@ -258,9 +269,11 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
||||
private func handleViewModelResult(_ result: ComposerViewModelResult) {
|
||||
switch result {
|
||||
case .cancel:
|
||||
self.toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
|
||||
toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
|
||||
case let .contentDidChange(isEmpty):
|
||||
setVoiceMessageToolbarIsHidden(!isEmpty)
|
||||
case let .linkTapped(linkAction):
|
||||
toolbarViewDelegate?.didSendLinkAction(LinkActionWrapper(linkAction))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +299,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
|
||||
private func update(theme: Theme) {
|
||||
hostingViewController.view.backgroundColor = theme.colors.background
|
||||
wysiwygViewModel.textColor = theme.colors.primaryContent
|
||||
wysiwygViewModel.linkColor = theme.colors.accent
|
||||
wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor
|
||||
}
|
||||
|
||||
private func updateTextViewHeight() {
|
||||
|
||||
@@ -137,12 +137,14 @@ class VoiceMessageAttachmentCacheManager {
|
||||
durations.removeAll()
|
||||
finalURLs.removeAll()
|
||||
|
||||
if FileManager.default.fileExists(atPath: temporaryFilesFolderURL.path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: temporaryFilesFolderURL)
|
||||
} catch {
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] Failed clearing cached disk files", context: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<VoiceMessageAttachmentCacheManagerLoadResult, Error>) -> Void) {
|
||||
let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: numberOfSamples)
|
||||
|
||||
@@ -20,6 +20,7 @@ import DSWaveformImage
|
||||
|
||||
@objc public protocol VoiceMessageControllerDelegate: AnyObject {
|
||||
func voiceMessageControllerDidRequestMicrophonePermission(_ voiceMessageController: VoiceMessageController)
|
||||
func voiceMessageControllerDidRequestRecording(_ voiceMessageController: VoiceMessageController) -> Bool
|
||||
func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, duration: UInt, samples: [Float]?, completion: @escaping (Bool) -> Void)
|
||||
}
|
||||
|
||||
@@ -106,6 +107,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
guard let temporaryFileURL = temporaryFileURL else {
|
||||
return
|
||||
}
|
||||
|
||||
// Ask our delegate if we can start recording
|
||||
let canStartRecording = delegate?.voiceMessageControllerDidRequestRecording(self) ?? true
|
||||
guard canStartRecording else {
|
||||
return
|
||||
}
|
||||
|
||||
guard AVAudioSession.sharedInstance().recordPermission == .granted else {
|
||||
delegate?.voiceMessageControllerDidRequestMicrophonePermission(self)
|
||||
return
|
||||
@@ -364,7 +372,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
}
|
||||
|
||||
private func deleteRecordingAtURL(_ url: URL?) {
|
||||
guard let url = url else {
|
||||
// Fix: use url.path instead of url.absoluteString when using FileManager otherwise the url seems to be percent encoded and the file is not found.
|
||||
guard let url = url, FileManager.default.fileExists(atPath: url.path) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ final class ThreadListViewController: UIViewController {
|
||||
|
||||
private func renderLoading() {
|
||||
emptyView.isHidden = true
|
||||
threadsTableView.isHidden = true
|
||||
threadsTableView.isHidden = viewModel.numberOfThreads == 0
|
||||
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
|
||||
}
|
||||
|
||||
@@ -352,6 +352,10 @@ extension ThreadListViewController: UITableViewDelegate {
|
||||
cell.backgroundColor = theme.backgroundColor
|
||||
cell.selectedBackgroundView = UIView()
|
||||
cell.selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor
|
||||
|
||||
if indexPath.row == viewModel.numberOfThreads - 1 {
|
||||
viewModel.process(viewAction: .loadData)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
@@ -29,7 +29,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
private var threads: [MXThreadProtocol] = []
|
||||
private var eventFormatter: MXKEventFormatter?
|
||||
private var roomState: MXRoomState?
|
||||
|
||||
private var nextBatch: String?
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
private var longPressedThread: MXThreadProtocol?
|
||||
|
||||
@@ -71,6 +71,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
viewState = .showingFilterTypes
|
||||
case .selectFilterType(let type):
|
||||
selectedFilterType = type
|
||||
resetData()
|
||||
loadData()
|
||||
case .selectThread(let index):
|
||||
selectThread(index)
|
||||
@@ -230,7 +231,15 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
)
|
||||
}
|
||||
|
||||
private func resetData() {
|
||||
nextBatch = nil
|
||||
threads = []
|
||||
}
|
||||
|
||||
private func loadData(showLoading: Bool = true) {
|
||||
guard threads.isEmpty || nextBatch != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if showLoading {
|
||||
viewState = .loading
|
||||
@@ -245,12 +254,12 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
onlyParticipated = true
|
||||
}
|
||||
|
||||
session.threadingService.allThreads(inRoom: roomId,
|
||||
onlyParticipated: onlyParticipated) { [weak self] response in
|
||||
session.threadingService.allThreads(inRoom: roomId, from: nextBatch, onlyParticipated: onlyParticipated) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
switch response {
|
||||
case .success(let threads):
|
||||
self.threads = threads
|
||||
case .success(let value):
|
||||
self.threads = self.threads + value.threads
|
||||
self.nextBatch = value.nextBatch
|
||||
self.threadsLoaded()
|
||||
case .failure(let error):
|
||||
MXLog.error("[ThreadListViewModel] loadData", context: error)
|
||||
|
||||
@@ -68,9 +68,7 @@ public class VoiceBroadcastAggregator {
|
||||
public var delegate: VoiceBroadcastAggregatorDelegate?
|
||||
|
||||
deinit {
|
||||
if let referenceEventsListener = referenceEventsListener {
|
||||
room.removeListener(referenceEventsListener)
|
||||
}
|
||||
self.stop()
|
||||
}
|
||||
|
||||
public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfoState) throws {
|
||||
@@ -103,6 +101,10 @@ public class VoiceBroadcastAggregator {
|
||||
currentUserIdentifier: session.myUserId)
|
||||
}
|
||||
|
||||
private func registerEventDidDecryptNotification() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(eventDidDecrypt), name: NSNotification.Name.mxEventDidDecrypt, object: nil)
|
||||
}
|
||||
|
||||
@objc private func handleRoomDataFlush(sender: Notification) {
|
||||
guard let room = sender.object as? MXRoom, room == self.room else {
|
||||
return
|
||||
@@ -112,9 +114,54 @@ public class VoiceBroadcastAggregator {
|
||||
MXLog.warning("[VoiceBroadcastAggregator] handleRoomDataFlush is not supported yet")
|
||||
}
|
||||
|
||||
@objc private func eventDidDecrypt(sender: Notification) {
|
||||
guard let event = sender.object as? MXEvent else { return }
|
||||
|
||||
self.handleEvent(event: event)
|
||||
}
|
||||
|
||||
private func handleEvent(event: MXEvent, direction: MXTimelineDirection? = nil, roomState: MXRoomState? = nil) {
|
||||
switch event.eventType {
|
||||
case .roomMessage:
|
||||
self.updateVoiceBroadcast(event: event)
|
||||
case .custom:
|
||||
if event.type == VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType {
|
||||
self.updateState()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVoiceBroadcast(event: MXEvent) {
|
||||
guard event.sender == self.voiceBroadcastSenderId,
|
||||
let relatedEventId = event.relatesTo?.eventId,
|
||||
relatedEventId == self.voiceBroadcastStartEventId,
|
||||
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.events.contains(where: { $0.eventId == event.eventId }) {
|
||||
self.events.append(event)
|
||||
MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
|
||||
|
||||
if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
|
||||
self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
|
||||
}
|
||||
|
||||
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
|
||||
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
|
||||
voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
|
||||
events: self.events,
|
||||
currentUserIdentifier: self.session.myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
self.room.state { roomState in
|
||||
guard let event = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last,
|
||||
// This update is useful only in case of a live broadcast (The aggregator considers the broadcast stopped by default)
|
||||
// We will consider here only the most recent voice broadcast state event
|
||||
self.room.lastVoiceBroadcastStateEvent { event in
|
||||
guard let event = event,
|
||||
event.stateKey == self.voiceBroadcastSenderId,
|
||||
let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content),
|
||||
(event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.voiceBroadcastId == self.voiceBroadcastStartEventId),
|
||||
@@ -149,38 +196,10 @@ public class VoiceBroadcastAggregator {
|
||||
self.events.append(contentsOf: filteredChunk)
|
||||
|
||||
let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage]
|
||||
self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes) { [weak self] event, direction, state in
|
||||
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if event.eventType == .roomMessage {
|
||||
guard event.sender == self.voiceBroadcastSenderId,
|
||||
let relatedEventId = event.relatesTo?.eventId,
|
||||
relatedEventId == self.voiceBroadcastStartEventId,
|
||||
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.events.contains(where: { $0.eventId == event.eventId }) {
|
||||
self.events.append(event)
|
||||
MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
|
||||
|
||||
if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
|
||||
self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
|
||||
}
|
||||
|
||||
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
|
||||
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
|
||||
voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
|
||||
events: self.events,
|
||||
currentUserIdentifier: self.session.myUserId)
|
||||
}
|
||||
} else {
|
||||
self.updateState()
|
||||
}
|
||||
} as Any
|
||||
self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes, onEvent: { [weak self] event, direction, roomState in
|
||||
self?.handleEvent(event: event, direction: direction, roomState: roomState)
|
||||
}) as Any
|
||||
self.registerEventDidDecryptNotification()
|
||||
|
||||
self.events.forEach { event in
|
||||
guard let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) else {
|
||||
@@ -212,4 +231,13 @@ public class VoiceBroadcastAggregator {
|
||||
self.delegate?.voiceBroadcastAggregator(self, didFailWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if let referenceEventsListener = referenceEventsListener {
|
||||
room.removeListener(referenceEventsListener)
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.mxEventDidDecrypt, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.mxRoomDidFlushData, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,14 @@ public class VoiceBroadcastService: NSObject {
|
||||
/// stop a voice broadcast info.
|
||||
/// - Parameters:
|
||||
/// - lastChunkSequence: The last sent chunk number.
|
||||
/// - voiceBroadcastId: The VoiceBroadcast identifier to stop. Use it only to force stop a specific VoiceBroadcast.
|
||||
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
|
||||
func stopVoiceBroadcast(lastChunkSequence: Int, completion: @escaping (MXResponse<String?>) -> Void) {
|
||||
func stopVoiceBroadcast(lastChunkSequence: Int,
|
||||
voiceBroadcastId: String? = nil,
|
||||
completion: @escaping (MXResponse<String?>) -> Void) {
|
||||
if let voiceBroadcastId = voiceBroadcastId {
|
||||
self.voiceBroadcastId = voiceBroadcastId
|
||||
}
|
||||
sendVoiceBroadcastInfo(lastChunkSequence: lastChunkSequence, state: VoiceBroadcastInfoState.stopped, completion: completion)
|
||||
}
|
||||
|
||||
@@ -132,7 +138,7 @@ public class VoiceBroadcastService: NSObject {
|
||||
case .resumed:
|
||||
return [.paused, .stopped]
|
||||
case .stopped:
|
||||
return [.started]
|
||||
return [.started, .stopped]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -556,6 +556,40 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
|
||||
}
|
||||
|
||||
#pragma mark - MXRoomSummaryUpdating
|
||||
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState {
|
||||
|
||||
// Do not display voice broadcast chunk in last message.
|
||||
if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Update last message if we have a voice broadcast in the room.
|
||||
if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
|
||||
{
|
||||
return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:event roomState:roomState];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject;
|
||||
if (stateEvent && ![VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state])
|
||||
{
|
||||
return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:stateEvent roomState:roomState];
|
||||
}
|
||||
}
|
||||
|
||||
BOOL updated = [super session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState];
|
||||
|
||||
if (updated) {
|
||||
// Force the default text color for the last message (cancel highlighted message color)
|
||||
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText];
|
||||
[lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor range:NSMakeRange(0, lastEventDescription.length)];
|
||||
summary.lastMessage.attributedText = lastEventDescription;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
- (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];
|
||||
@@ -579,6 +613,67 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
|
||||
return updated;
|
||||
}
|
||||
|
||||
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent roomState:(MXRoomState *)roomState
|
||||
{
|
||||
[summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:stateEvent]];
|
||||
if (summary.lastMessage.others == nil)
|
||||
{
|
||||
summary.lastMessage.others = [NSMutableDictionary dictionary];
|
||||
}
|
||||
summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES];
|
||||
|
||||
NSAttributedString *attachmentString = nil;
|
||||
UIColor *textColor;
|
||||
if ([VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state])
|
||||
{
|
||||
textColor = ThemeService.shared.theme.textSecondaryColor;
|
||||
NSString *senderDisplayName;
|
||||
if ([stateEvent.stateKey isEqualToString:session.myUser.userId])
|
||||
{
|
||||
summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastEndedByYou;
|
||||
}
|
||||
else
|
||||
{
|
||||
senderDisplayName = [self senderDisplayNameForEvent:stateEvent withRoomState:roomState];
|
||||
summary.lastMessage.text = [VectorL10n noticeVoiceBroadcastEnded:senderDisplayName];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
textColor = ThemeService.shared.theme.colors.alert;
|
||||
UIImage *liveImage = AssetImages.voiceBroadcastLive.image;
|
||||
|
||||
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
|
||||
attachment.image = [liveImage imageWithTintColor:textColor renderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
|
||||
|
||||
summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastLive;
|
||||
}
|
||||
|
||||
// Compute the attribute text message
|
||||
NSMutableAttributedString *lastMessage;
|
||||
if (attachmentString)
|
||||
{
|
||||
lastMessage = [[NSMutableAttributedString alloc] initWithAttributedString:attachmentString];
|
||||
// Change base line
|
||||
[lastMessage addAttribute:NSBaselineOffsetAttributeName value:@(-3.0f) range:NSMakeRange(0, attachmentString.length)];
|
||||
|
||||
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", summary.lastMessage.text]];
|
||||
[lastMessage appendAttributedString:attributedText];
|
||||
[lastMessage addAttribute:NSFontAttributeName value:self.defaultTextFont range:NSMakeRange(0, lastMessage.length)];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSAttributedString *attributedText = [self renderString:summary.lastMessage.text forEvent:stateEvent];
|
||||
lastMessage = [[NSMutableAttributedString alloc] initWithAttributedString:attributedText];
|
||||
}
|
||||
|
||||
[lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)];
|
||||
summary.lastMessage.attributedText = lastMessage;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)redactedMessageReplacementAttributedString
|
||||
{
|
||||
UIFont *font = self.defaultTextFont;
|
||||
|
||||
@@ -63,6 +63,10 @@ class HTMLFormatter: NSObject {
|
||||
MXKTools.removeDTCoreTextArtifacts(mutableString)
|
||||
postFormatOperations?(mutableString)
|
||||
|
||||
// Remove CTForegroundColorFromContext attribute to fix the iOS 16 black link color issue
|
||||
// REF: https://github.com/Cocoanetics/DTCoreText/issues/792
|
||||
mutableString.removeAttribute(NSAttributedString.Key("CTForegroundColorFromContext"), range: NSRange(location: 0, length: mutableString.length))
|
||||
|
||||
return mutableString
|
||||
}
|
||||
|
||||
|
||||
@@ -553,7 +553,14 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
notificationBody = NotificationService.localizedString(forKey: "VIDEO_FROM_USER", eventSenderName)
|
||||
case kMXMessageTypeAudio:
|
||||
if event.isVoiceMessage() {
|
||||
// Ignore voice broadcast chunk event except the first one.
|
||||
if let chunkInfo = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] as? [String: UInt] {
|
||||
if chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence] == 1 {
|
||||
notificationBody = NotificationService.localizedString(forKey: "VOICE_BROADCAST_FROM_USER", eventSenderName)
|
||||
}
|
||||
} else {
|
||||
notificationBody = NotificationService.localizedString(forKey: "VOICE_MESSAGE_FROM_USER", eventSenderName)
|
||||
}
|
||||
} else {
|
||||
notificationBody = NotificationService.localizedString(forKey: "AUDIO_FROM_USER", eventSenderName, messageContent)
|
||||
}
|
||||
@@ -668,6 +675,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
notificationTitle = nil
|
||||
notificationBody = NotificationService.localizedString(forKey: "MESSAGE_PROTECTED")
|
||||
}
|
||||
|
||||
case .pollStart:
|
||||
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
||||
notificationBody = MXEventContentPollStart(fromJSON: event.content)?.question
|
||||
@@ -678,11 +686,15 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
notificationTitle = nil
|
||||
notificationBody = NotificationService.localizedString(forKey: "MESSAGE_PROTECTED")
|
||||
}
|
||||
case .pollEnd:
|
||||
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
|
||||
notificationBody = VectorL10n.pollTimelineEndedText
|
||||
// bwi 1.9.15: hide content in pollend notifications
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
self.validateNotificationContentAndComplete(
|
||||
notificationTitle: notificationTitle,
|
||||
notificationBody: notificationBody,
|
||||
|
||||
@@ -63,6 +63,7 @@ targets:
|
||||
- path: ../Riot/Managers/Locale/LocaleProviderType.swift
|
||||
- path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift
|
||||
- path: ../Riot/Categories/Bundle.swift
|
||||
- path: ../Riot/Categories/MXEvent.swift
|
||||
- path: ../Riot/Generated/Strings.swift
|
||||
- path: ../Riot/Generated/BWIStrings.swift
|
||||
- path: ../Riot/Generated/Images.swift
|
||||
@@ -86,4 +87,4 @@ targets:
|
||||
excludes:
|
||||
- "**/*.md" # excludes all files with the .md extension
|
||||
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
|
||||
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift
|
||||
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
|
||||
|
||||
@@ -45,6 +45,7 @@ targets:
|
||||
- path: ../bwi/AppConfig
|
||||
- path: ../Riot/Modules/Common/SegmentedViewController/SegmentedViewController.m
|
||||
- path: ../Riot/Categories/Bundle.swift
|
||||
- path: ../Riot/Categories/MXEvent.swift
|
||||
- path: ../Riot/Managers/Theme/
|
||||
- path: ../Riot/Utils/AvatarGenerator.m
|
||||
- path: ../Config/BuildSettings.swift
|
||||
@@ -89,4 +90,4 @@ targets:
|
||||
excludes:
|
||||
- "**/*.md" # excludes all files with the .md extension
|
||||
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
|
||||
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift
|
||||
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
|
||||
|
||||
@@ -71,6 +71,7 @@ enum MockAppScreens {
|
||||
MockSpaceSelectorScreenState.self,
|
||||
MockComposerScreenState.self,
|
||||
MockComposerCreateActionListScreenState.self,
|
||||
MockComposerLinkActionScreenState.self,
|
||||
MockVoiceBroadcastPlaybackScreenState.self
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,11 +18,15 @@ import Foundation
|
||||
import XCTest
|
||||
|
||||
extension XCUIApplication {
|
||||
func goToScreenWithIdentifier(_ identifier: String) {
|
||||
func goToScreenWithIdentifier(_ identifier: String, shouldUseSlowTyping: Bool = false) {
|
||||
// Search for the screen identifier
|
||||
let textField = textFields["searchQueryTextField"]
|
||||
let button = buttons[identifier]
|
||||
|
||||
// This always fixes the stuck search issue, but makes the typing slower
|
||||
if shouldUseSlowTyping {
|
||||
textField.typeSlowly(identifier)
|
||||
} else {
|
||||
// Sometimes the search gets stuck without showing any results. Try to nudge it along
|
||||
for _ in 0...10 {
|
||||
textField.clearAndTypeText(identifier)
|
||||
@@ -30,12 +34,13 @@ extension XCUIApplication {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.tap()
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElement {
|
||||
extension XCUIElement {
|
||||
func clearAndTypeText(_ text: String) {
|
||||
guard let stringValue = value as? String else {
|
||||
XCTFail("Tried to clear and type text into a non string value")
|
||||
@@ -49,4 +54,9 @@ private extension XCUIElement {
|
||||
typeText(deleteString)
|
||||
typeText(text)
|
||||
}
|
||||
|
||||
func typeSlowly(_ text: String) {
|
||||
tap()
|
||||
text.forEach{ typeText(String($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,16 @@ enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable {
|
||||
case .fullList:
|
||||
actions = ComposerCreateAction.allCases
|
||||
}
|
||||
let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(
|
||||
let viewModel = ComposerCreateActionListViewModel(
|
||||
initialViewState: ComposerCreateActionListViewState(
|
||||
actions: actions,
|
||||
wysiwygEnabled: true,
|
||||
isScrollingEnabled: false,
|
||||
bindings: ComposerCreateActionListBindings(textFormattingEnabled: true)))
|
||||
bindings: ComposerCreateActionListBindings(
|
||||
textFormattingEnabled: true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2022 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 WysiwygComposer
|
||||
|
||||
protocol ComposerLinkActionBridgePresenterDelegate: AnyObject {
|
||||
func didCancel()
|
||||
func didDismissInteractively()
|
||||
func didRequestLinkOperation(_ linkOperation: WysiwygLinkOperation)
|
||||
}
|
||||
|
||||
final class ComposerLinkActionBridgePresenter: NSObject {
|
||||
private var coordinator: ComposerLinkActionCoordinator?
|
||||
private var linkAction: LinkAction
|
||||
|
||||
weak var delegate: ComposerLinkActionBridgePresenterDelegate?
|
||||
|
||||
init(linkAction: LinkActionWrapper) {
|
||||
self.linkAction = linkAction.linkAction
|
||||
super.init()
|
||||
}
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
let composerLinkActionCoordinator = ComposerLinkActionCoordinator(linkAction: linkAction)
|
||||
composerLinkActionCoordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .didTapCancel:
|
||||
self?.delegate?.didCancel()
|
||||
case .didDismissInteractively:
|
||||
self?.delegate?.didDismissInteractively()
|
||||
case let .didRequestLinkOperation(linkOperation):
|
||||
self?.delegate?.didRequestLinkOperation(linkOperation)
|
||||
}
|
||||
}
|
||||
let presentable = composerLinkActionCoordinator.toPresentable()
|
||||
viewController.present(presentable, animated: animated, completion: nil)
|
||||
composerLinkActionCoordinator.start()
|
||||
coordinator = composerLinkActionCoordinator
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||
guard let coordinator = coordinator else {
|
||||
return
|
||||
}
|
||||
// Dismiss modal
|
||||
coordinator.toPresentable().dismiss(animated: animated) {
|
||||
self.coordinator = nil
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright 2022 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 WysiwygComposer
|
||||
|
||||
enum ComposerLinkActionCoordinatorAction {
|
||||
case didTapCancel
|
||||
case didDismissInteractively
|
||||
case didRequestLinkOperation(_ linkOperation: WysiwygLinkOperation)
|
||||
}
|
||||
|
||||
final class ComposerLinkActionCoordinator: NSObject, Coordinator, Presentable {
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
private let hostingController: UIViewController
|
||||
private let viewModel: ComposerLinkActionViewModel
|
||||
|
||||
var callback: ((ComposerLinkActionCoordinatorAction) -> Void)?
|
||||
|
||||
init(linkAction: LinkAction) {
|
||||
viewModel = ComposerLinkActionViewModel(from: linkAction)
|
||||
hostingController = VectorHostingController(rootView: ComposerLinkAction(viewModel: viewModel.context))
|
||||
super.init()
|
||||
hostingController.presentationController?.delegate = self
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.callback = { [weak self] result in
|
||||
switch result {
|
||||
case .cancel:
|
||||
self?.callback?(.didTapCancel)
|
||||
case let .performOperation(linkOperation):
|
||||
self?.callback?(.didRequestLinkOperation(linkOperation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
hostingController
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposerLinkActionCoordinator: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.callback?(.didDismissInteractively)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum MockComposerLinkActionScreenState: MockScreenState, CaseIterable {
|
||||
case edit
|
||||
case createWithText
|
||||
case create
|
||||
|
||||
var screenType: Any.Type {
|
||||
ComposerLinkAction.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: ComposerLinkActionViewModel
|
||||
switch self {
|
||||
case .createWithText:
|
||||
viewModel = .init(from: .createWithText)
|
||||
case .create:
|
||||
viewModel = .init(from: .create)
|
||||
case .edit:
|
||||
viewModel = .init(from: .edit(link: "https://element.io"))
|
||||
}
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(ComposerLinkAction(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright 2022 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 WysiwygComposer
|
||||
|
||||
enum ComposerLinkActionViewAction: Equatable {
|
||||
case cancel
|
||||
case save
|
||||
case remove
|
||||
}
|
||||
|
||||
enum ComposerLinkActionViewModelResult: Equatable {
|
||||
case cancel
|
||||
case performOperation(_ linkOperation: WysiwygLinkOperation)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct ComposerLinkActionViewState: BindableState {
|
||||
let linkAction: LinkAction
|
||||
|
||||
var bindings: ComposerLinkActionBindings
|
||||
}
|
||||
|
||||
extension ComposerLinkActionViewState {
|
||||
var title: String {
|
||||
switch linkAction {
|
||||
case .createWithText, .create: return VectorL10n.wysiwygComposerLinkActionCreateTitle
|
||||
case .edit: return VectorL10n.wysiwygComposerLinkActionEditTitle
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDisplayTextField: Bool {
|
||||
switch linkAction {
|
||||
case .createWithText: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDisplayRemoveButton: Bool {
|
||||
switch linkAction {
|
||||
case .edit: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var isSaveButtonDisabled: Bool {
|
||||
guard isValidLink else { return true }
|
||||
switch linkAction {
|
||||
case .createWithText: return bindings.text.isEmpty
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
private var isValidLink: Bool {
|
||||
guard let url = URL(string: bindings.linkUrl) else { return false }
|
||||
return UIApplication.shared.canOpenURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposerLinkActionBindings {
|
||||
var text: String
|
||||
var linkUrl: String
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright 2022 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 RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
final class ComposerLinkActionUITests: MockScreenTestCase {
|
||||
func testCreate() {
|
||||
app.goToScreenWithIdentifier(MockComposerLinkActionScreenState.create.title, shouldUseSlowTyping: true)
|
||||
XCTAssertFalse(app.buttons[VectorL10n.remove].exists)
|
||||
XCTAssertTrue(app.buttons[VectorL10n.cancel].exists)
|
||||
let saveButton = app.buttons[VectorL10n.save]
|
||||
XCTAssertTrue(saveButton.exists)
|
||||
XCTAssertFalse(saveButton.isEnabled)
|
||||
XCTAssertFalse(app.textFields["textTextField"].exists)
|
||||
let linkTextField = app.textFields["linkTextField"]
|
||||
XCTAssertTrue(linkTextField.exists)
|
||||
linkTextField.tap()
|
||||
linkTextField.typeText("invalid url")
|
||||
XCTAssertFalse(saveButton.isEnabled)
|
||||
linkTextField.clearAndTypeText("https://element.io")
|
||||
XCTAssertTrue(saveButton.isEnabled)
|
||||
}
|
||||
|
||||
func testCreateWithText() {
|
||||
app.goToScreenWithIdentifier(MockComposerLinkActionScreenState.createWithText.title, shouldUseSlowTyping: true)
|
||||
XCTAssertFalse(app.buttons[VectorL10n.remove].exists)
|
||||
XCTAssertTrue(app.buttons[VectorL10n.cancel].exists)
|
||||
let saveButton = app.buttons[VectorL10n.save]
|
||||
XCTAssertTrue(saveButton.exists)
|
||||
XCTAssertFalse(saveButton.isEnabled)
|
||||
let textTextField = app.textFields["textTextField"]
|
||||
XCTAssertTrue(textTextField.exists)
|
||||
let linkTextField = app.textFields["linkTextField"]
|
||||
XCTAssertTrue(linkTextField.exists)
|
||||
linkTextField.tap()
|
||||
linkTextField.typeText("https://element.io")
|
||||
XCTAssertFalse(saveButton.isEnabled)
|
||||
textTextField.tap()
|
||||
textTextField.typeText("test")
|
||||
XCTAssertTrue(saveButton.isEnabled)
|
||||
}
|
||||
|
||||
func testEdit() {
|
||||
app.goToScreenWithIdentifier(MockComposerLinkActionScreenState.edit.title, shouldUseSlowTyping: true)
|
||||
XCTAssertTrue(app.buttons[VectorL10n.remove].exists)
|
||||
XCTAssertTrue(app.buttons[VectorL10n.cancel].exists)
|
||||
let saveButton = app.buttons[VectorL10n.save]
|
||||
XCTAssertTrue(saveButton.exists)
|
||||
XCTAssertTrue(saveButton.isEnabled)
|
||||
XCTAssertFalse(app.textFields["textTextField"].exists)
|
||||
let linkTextField = app.textFields["linkTextField"]
|
||||
XCTAssertTrue(linkTextField.exists)
|
||||
let value = linkTextField.value as? String
|
||||
XCTAssertEqual(value, "https://element.io")
|
||||
linkTextField.clearAndTypeText("invalid url")
|
||||
XCTAssertFalse(saveButton.isEnabled)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// Copyright 2022 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.
|
||||
//
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
import WysiwygComposer
|
||||
import XCTest
|
||||
|
||||
final class ComposerLinkActionViewModelTests: XCTestCase {
|
||||
var viewModel: ComposerLinkActionViewModel!
|
||||
var context: ComposerLinkActionViewModel.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = nil
|
||||
context = nil
|
||||
}
|
||||
|
||||
private func setUp(with linkAction: LinkAction) {
|
||||
viewModel = ComposerLinkActionViewModel(from: linkAction)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testCreateWithTextDefaultState() {
|
||||
setUp(with: .createWithText)
|
||||
XCTAssertEqual(context.viewState.bindings.text, "")
|
||||
XCTAssertEqual(context.viewState.bindings.linkUrl, "")
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
XCTAssertFalse(context.viewState.shouldDisplayRemoveButton)
|
||||
XCTAssertTrue(context.viewState.shouldDisplayTextField)
|
||||
XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionCreateTitle)
|
||||
}
|
||||
|
||||
func testCreateDefaultState() {
|
||||
setUp(with: .create)
|
||||
XCTAssertEqual(context.viewState.bindings.text, "")
|
||||
XCTAssertEqual(context.viewState.bindings.linkUrl, "")
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
XCTAssertFalse(context.viewState.shouldDisplayRemoveButton)
|
||||
XCTAssertFalse(context.viewState.shouldDisplayTextField)
|
||||
XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionCreateTitle)
|
||||
}
|
||||
|
||||
func testEditDefaultState() {
|
||||
let link = "https://element.io"
|
||||
setUp(with: .edit(link: link))
|
||||
XCTAssertEqual(context.viewState.bindings.text, "")
|
||||
XCTAssertEqual(context.viewState.bindings.linkUrl, link)
|
||||
XCTAssertFalse(context.viewState.isSaveButtonDisabled)
|
||||
XCTAssertTrue(context.viewState.shouldDisplayRemoveButton)
|
||||
XCTAssertFalse(context.viewState.shouldDisplayTextField)
|
||||
XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionEditTitle)
|
||||
}
|
||||
|
||||
func testUrlValidityCheck() {
|
||||
setUp(with: .create)
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
context.linkUrl = "invalid url"
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
context.linkUrl = "https://element.io"
|
||||
XCTAssertFalse(context.viewState.isSaveButtonDisabled)
|
||||
}
|
||||
|
||||
func testTextNotEmptyCheck() {
|
||||
setUp(with: .createWithText)
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
context.linkUrl = "https://element.io"
|
||||
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
|
||||
context.text = "text"
|
||||
XCTAssertFalse(context.viewState.isSaveButtonDisabled)
|
||||
}
|
||||
|
||||
func testCancelAction() {
|
||||
setUp(with: .create)
|
||||
var result: ComposerLinkActionViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
context.send(viewAction: .cancel)
|
||||
XCTAssertEqual(result, .cancel)
|
||||
}
|
||||
|
||||
func testRemoveAction() {
|
||||
setUp(with: .edit(link: "https://element.io"))
|
||||
var result: ComposerLinkActionViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
context.send(viewAction: .remove)
|
||||
XCTAssertEqual(result, .performOperation(.removeLinks))
|
||||
}
|
||||
|
||||
func testSaveActionForCreate() {
|
||||
setUp(with: .create)
|
||||
var result: ComposerLinkActionViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
let link = "https://element.io"
|
||||
context.linkUrl = link
|
||||
context.send(viewAction: .save)
|
||||
XCTAssertEqual(result, .performOperation(.setLink(urlString: link)))
|
||||
}
|
||||
|
||||
func testSaveActionForCreateWithText() {
|
||||
setUp(with: .createWithText)
|
||||
var result: ComposerLinkActionViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
let link = "https://element.io"
|
||||
context.linkUrl = link
|
||||
let text = "test"
|
||||
context.text = text
|
||||
context.send(viewAction: .save)
|
||||
XCTAssertEqual(result, .performOperation(.createLink(urlString: link, text: text)))
|
||||
}
|
||||
|
||||
func testSaveActionForEdit() {
|
||||
setUp(with: .edit(link: "https://element.io"))
|
||||
var result: ComposerLinkActionViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
let link = "https://matrix.org"
|
||||
context.linkUrl = link
|
||||
context.send(viewAction: .save)
|
||||
XCTAssertEqual(result, .performOperation(.setLink(urlString: link)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposerLinkAction: View {
|
||||
enum Field {
|
||||
case text
|
||||
case link
|
||||
}
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
@ObservedObject private var viewModel: ComposerLinkActionViewModel.Context
|
||||
|
||||
@State private var selectedField: Field?
|
||||
|
||||
private var isTextFocused: Bool {
|
||||
selectedField == .text
|
||||
}
|
||||
|
||||
private var isLinkFocused: Bool {
|
||||
selectedField == .link
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
if viewModel.viewState.shouldDisplayTextField {
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(VectorL10n.wysiwygComposerLinkActionText)
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.text,
|
||||
onEditingChanged: { edit in
|
||||
selectedField = edit ? .text : nil
|
||||
}
|
||||
)
|
||||
.textFieldStyle(BorderedInputFieldStyle(isEditing: isTextFocused))
|
||||
.autocapitalization(.none)
|
||||
.accessibilityIdentifier("textTextField")
|
||||
.accessibilityLabel(VectorL10n.wysiwygComposerLinkActionText)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(VectorL10n.wysiwygComposerLinkActionLink)
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
TextField(
|
||||
"",
|
||||
text: $viewModel.linkUrl,
|
||||
onEditingChanged: { edit in
|
||||
selectedField = edit ? .link : nil
|
||||
}
|
||||
)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.textFieldStyle(BorderedInputFieldStyle(isEditing: isLinkFocused))
|
||||
.accessibilityIdentifier("linkTextField")
|
||||
.accessibilityLabel(VectorL10n.wysiwygComposerLinkActionLink)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
VStack(spacing: 16) {
|
||||
Button(VectorL10n.save) {
|
||||
viewModel.send(viewAction: .save)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(viewModel.viewState.isSaveButtonDisabled)
|
||||
.animation(.easeInOut(duration: 0.15), value: viewModel.viewState.isSaveButtonDisabled)
|
||||
if viewModel.viewState.shouldDisplayRemoveButton {
|
||||
Button(VectorL10n.remove) {
|
||||
viewModel.send(viewAction: .remove)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.alert))
|
||||
}
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
}
|
||||
.buttonStyle(SecondaryActionButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.top, 40.0)
|
||||
.padding(.bottom, 12.0)
|
||||
.padding(.horizontal, 16.0)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(VectorL10n.cancel, action: {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(viewModel.viewState.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
|
||||
}
|
||||
.accentColor(theme.colors.accent)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
init(viewModel: ComposerLinkActionViewModel.Context) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposerLinkActionView_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockComposerLinkActionScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// Copyright 2022 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 WysiwygComposer
|
||||
|
||||
typealias ComposerLinkActionViewModelType = StateStoreViewModel<ComposerLinkActionViewState, ComposerLinkActionViewAction>
|
||||
|
||||
final class ComposerLinkActionViewModel: ComposerLinkActionViewModelType, ComposerLinkActionViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((ComposerLinkActionViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
init(from linkAction: LinkAction) {
|
||||
let initialViewState: ComposerLinkActionViewState
|
||||
let simpleBindings = ComposerLinkActionBindings(text: "", linkUrl: "")
|
||||
switch linkAction {
|
||||
case let .edit(link):
|
||||
initialViewState = .init(
|
||||
linkAction: .edit(link: link),
|
||||
bindings: .init(
|
||||
text: "",
|
||||
linkUrl: link
|
||||
)
|
||||
)
|
||||
case .createWithText:
|
||||
initialViewState = .init(linkAction: .createWithText, bindings: simpleBindings)
|
||||
case .create:
|
||||
initialViewState = .init(linkAction: .create, bindings: simpleBindings)
|
||||
}
|
||||
|
||||
super.init(initialViewState: initialViewState)
|
||||
}
|
||||
|
||||
override func process(viewAction: ComposerLinkActionViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
case .remove:
|
||||
callback?(.performOperation(.removeLinks))
|
||||
case .save:
|
||||
switch state.linkAction {
|
||||
case .createWithText:
|
||||
callback?(
|
||||
.performOperation(
|
||||
.createLink(
|
||||
urlString: state.bindings.linkUrl,
|
||||
text: state.bindings.text
|
||||
)
|
||||
)
|
||||
)
|
||||
case .create, .edit:
|
||||
callback?(
|
||||
.performOperation(
|
||||
.setLink(urlString: state.bindings.linkUrl)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
protocol ComposerLinkActionViewModelProtocol {
|
||||
var context: ComposerLinkActionViewModelType.Context { get }
|
||||
var callback: ((ComposerLinkActionViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
@@ -24,10 +24,8 @@ import WysiwygComposer
|
||||
struct FormatItem {
|
||||
/// The type of the item
|
||||
let type: FormatType
|
||||
/// Whether it is active(highlighted)
|
||||
let active: Bool
|
||||
/// Whether it is disabled or enabled
|
||||
let disabled: Bool
|
||||
/// The state of the item
|
||||
let state: ActionState
|
||||
}
|
||||
|
||||
/// The types of formatting actions
|
||||
@@ -36,6 +34,8 @@ enum FormatType {
|
||||
case italic
|
||||
case underline
|
||||
case strikethrough
|
||||
case inlineCode
|
||||
case link
|
||||
}
|
||||
|
||||
extension FormatType: CaseIterable, Identifiable {
|
||||
@@ -58,6 +58,10 @@ extension FormatItem {
|
||||
return Asset.Images.strikethrough.name
|
||||
case .underline:
|
||||
return Asset.Images.underlined.name
|
||||
case .link:
|
||||
return Asset.Images.link.name
|
||||
case .inlineCode:
|
||||
return Asset.Images.code.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +75,10 @@ extension FormatItem {
|
||||
return "strikethroughButton"
|
||||
case .underline:
|
||||
return "underlineButton"
|
||||
case .link:
|
||||
return "linkButton"
|
||||
case .inlineCode:
|
||||
return "inlineCodeButton"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +92,10 @@ extension FormatItem {
|
||||
return VectorL10n.wysiwygComposerFormatActionStrikethrough
|
||||
case .underline:
|
||||
return VectorL10n.wysiwygComposerFormatActionUnderline
|
||||
case .link:
|
||||
return VectorL10n.wysiwygComposerFormatActionLink
|
||||
case .inlineCode:
|
||||
return VectorL10n.wysiwygComposerFormatActionInlineCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,11 +112,14 @@ extension FormatType {
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
case .link:
|
||||
return .link
|
||||
case .inlineCode:
|
||||
return .inlineCode
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We probably don't need to expose this, clean up.
|
||||
|
||||
/// Convenience method to map it to the external rust binging action
|
||||
var composerAction: ComposerAction {
|
||||
switch self {
|
||||
@@ -116,6 +131,10 @@ extension FormatType {
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
case .link:
|
||||
return .link
|
||||
case .inlineCode:
|
||||
return .inlineCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,11 +149,23 @@ enum ComposerSendMode: Equatable {
|
||||
enum ComposerViewAction: Equatable {
|
||||
case cancel
|
||||
case contentDidChange(isEmpty: Bool)
|
||||
case linkTapped(linkAction: LinkAction)
|
||||
case storeSelection(selection: NSRange)
|
||||
}
|
||||
|
||||
enum ComposerViewModelResult: Equatable {
|
||||
case cancel
|
||||
case contentDidChange(isEmpty: Bool)
|
||||
case linkTapped(LinkAction: LinkAction)
|
||||
}
|
||||
|
||||
final class LinkActionWrapper: NSObject {
|
||||
let linkAction: LinkAction
|
||||
|
||||
init(_ linkAction: LinkAction) {
|
||||
self.linkAction = linkAction
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -81,4 +81,24 @@ final class ComposerViewModelTests: XCTestCase {
|
||||
viewModel.dismissKeyboard()
|
||||
XCTAssert(context.viewState.bindings.focused == false)
|
||||
}
|
||||
|
||||
func testSelectionToRestore() {
|
||||
XCTAssertEqual(viewModel.selectionToRestore, nil)
|
||||
let testRange = NSRange(location: 0, length: 10)
|
||||
context.send(viewAction: .storeSelection(selection: testRange))
|
||||
XCTAssertEqual(viewModel.selectionToRestore, testRange)
|
||||
}
|
||||
|
||||
func testLinkAction() {
|
||||
var result: ComposerViewModelResult!
|
||||
viewModel.callback = { value in
|
||||
result = value
|
||||
}
|
||||
context.send(viewAction: .linkTapped(linkAction: .createWithText))
|
||||
XCTAssertEqual(result, .linkTapped(LinkAction: .createWithText))
|
||||
context.send(viewAction: .linkTapped(linkAction: .create))
|
||||
XCTAssertEqual(result, .linkTapped(LinkAction: .create))
|
||||
context.send(viewAction: .linkTapped(linkAction: .edit(link: "https://element.io")))
|
||||
XCTAssertEqual(result, .linkTapped(LinkAction: .edit(link: "https://element.io")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,7 @@ struct Composer: View {
|
||||
FormatType.allCases.map { type in
|
||||
FormatItem(
|
||||
type: type,
|
||||
active: wysiwygViewModel.actionStates[type.composerAction] == .reversed,
|
||||
disabled: wysiwygViewModel.actionStates[type.composerAction] == .disabled
|
||||
state: wysiwygViewModel.actionStates[type.composerAction] ?? .disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -226,8 +225,13 @@ struct Composer: View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
sendMediaButton
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
if type.action == .link {
|
||||
storeCurrentSelection()
|
||||
sendLinkAction()
|
||||
} else {
|
||||
wysiwygViewModel.apply(type.action)
|
||||
}
|
||||
}
|
||||
.frame(height: 44)
|
||||
Spacer()
|
||||
sendButton
|
||||
@@ -242,6 +246,15 @@ struct Composer: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func storeCurrentSelection() {
|
||||
viewModel.send(viewAction: .storeSelection(selection: wysiwygViewModel.attributedContent.selection))
|
||||
}
|
||||
|
||||
private func sendLinkAction() {
|
||||
let linkAction = wysiwygViewModel.getLinkAction()
|
||||
viewModel.send(viewAction: .linkTapped(linkAction: linkAction))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Previews
|
||||
|
||||
@@ -39,28 +39,40 @@ struct FormattingToolbar: View {
|
||||
} label: {
|
||||
Image(item.icon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(item.active ? theme.colors.accent : theme.colors.tertiaryContent)
|
||||
.foregroundColor(getForegroundColor(for: item))
|
||||
}
|
||||
.disabled(item.disabled)
|
||||
.disabled(item.state == .disabled)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(item.active ? theme.colors.accent.opacity(0.1) : theme.colors.background)
|
||||
.background(getBackgroundColor(for: item))
|
||||
.cornerRadius(8)
|
||||
.accessibilityIdentifier(item.accessibilityIdentifier)
|
||||
.accessibilityLabel(item.accessibilityLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getForegroundColor(for item: FormatItem) -> Color {
|
||||
switch item.state {
|
||||
case .reversed: return theme.colors.accent
|
||||
case .enabled: return theme.colors.tertiaryContent
|
||||
case .disabled: return theme.colors.tertiaryContent.opacity(0.3)
|
||||
}
|
||||
}
|
||||
|
||||
private func getBackgroundColor(for item: FormatItem) -> Color {
|
||||
switch item.state {
|
||||
case .reversed: return theme.colors.accent.opacity(0.1)
|
||||
default: return theme.colors.background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct FormattingToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FormattingToolbar(formatItems: [
|
||||
FormatItem(type: .bold, active: true, disabled: false),
|
||||
FormatItem(type: .italic, active: false, disabled: false),
|
||||
FormatItem(type: .strikethrough, active: true, disabled: false),
|
||||
FormatItem(type: .underline, active: false, disabled: true)
|
||||
], formatAction: { _ in })
|
||||
FormattingToolbar(
|
||||
formatItems: FormatType.allCases.map { FormatItem(type: $0, state: .enabled) }
|
||||
, formatAction: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
|
||||
state.bindings.focused
|
||||
}
|
||||
|
||||
var selectionToRestore: NSRange?
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ComposerViewAction) {
|
||||
@@ -84,6 +86,10 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
|
||||
callback?(.cancel)
|
||||
case let .contentDidChange(isEmpty):
|
||||
callback?(.contentDidChange(isEmpty: isEmpty))
|
||||
case let .linkTapped(linkAction):
|
||||
callback?(.linkTapped(LinkAction: linkAction))
|
||||
case let .storeSelection(selection):
|
||||
selectionToRestore = selection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ protocol ComposerViewModelProtocol {
|
||||
var placeholder: String? { get set }
|
||||
var isFocused: Bool { get }
|
||||
var isLandscapePhone: Bool { get set }
|
||||
var selectionToRestore: NSRange? { get }
|
||||
|
||||
func dismissKeyboard()
|
||||
func showKeyboard()
|
||||
|
||||
@@ -21,7 +21,7 @@ import SwiftUI
|
||||
struct TimelinePollCoordinatorParameters {
|
||||
let session: MXSession
|
||||
let room: MXRoom
|
||||
let pollStartEvent: MXEvent
|
||||
let pollEvent: MXEvent
|
||||
}
|
||||
|
||||
final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate {
|
||||
@@ -46,7 +46,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
init(parameters: TimelinePollCoordinatorParameters) throws {
|
||||
self.parameters = parameters
|
||||
|
||||
try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId)
|
||||
try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent)
|
||||
pollAggregator.delegate = self
|
||||
|
||||
viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll))
|
||||
@@ -65,7 +65,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
.sink { [weak self] identifiers in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.parameters.room.sendPollResponse(for: parameters.pollStartEvent,
|
||||
self.parameters.room.sendPollResponse(for: parameters.pollEvent,
|
||||
withAnswerIdentifiers: identifiers,
|
||||
threadId: nil,
|
||||
localEcho: nil, success: nil) { [weak self] error in
|
||||
@@ -96,7 +96,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
}
|
||||
|
||||
func endPoll() {
|
||||
parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] _ in
|
||||
parameters.room.sendPollEnd(for: parameters.pollEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] _ in
|
||||
self?.viewModel.showClosingFailure()
|
||||
}
|
||||
}
|
||||
@@ -131,8 +131,10 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
closed: poll.isClosed,
|
||||
totalAnswerCount: poll.totalAnswerCount,
|
||||
type: pollKindToTimelinePollType(poll.kind),
|
||||
eventType: parameters.pollEvent.eventType == .pollStart ? .started : .ended,
|
||||
maxAllowedSelections: poll.maxAllowedSelections,
|
||||
hasBeenEdited: poll.hasBeenEdited)
|
||||
hasBeenEdited: poll.hasBeenEdited,
|
||||
hasDecryptionError: poll.hasDecryptionError)
|
||||
}
|
||||
|
||||
private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType {
|
||||
|
||||
@@ -43,7 +43,7 @@ class TimelinePollProvider: NSObject {
|
||||
return coordinator.toPresentable()
|
||||
}
|
||||
|
||||
let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event)
|
||||
let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event)
|
||||
guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else {
|
||||
return nil
|
||||
}
|
||||
|
||||