Merge branch 'develop' into feature/bma/scroll-to-unread-messages
This commit is contained in:
@@ -336,7 +336,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
||||
upload_reports:
|
||||
name: Project Check Suite
|
||||
|
||||
+97
@@ -1,3 +1,92 @@
|
||||
Changes in Element X v26.06.1
|
||||
=============================
|
||||
|
||||
The release of `v26.06.0` was cancelled because it contained some changes that would prevent some users with devices having no Vulkan support from being able to install the app. This release contains the same changes as `v26.06.0` but with the Vulkan requirement removed.
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Image edition before sending: crop and rotate. by @bmarty in https://github.com/element-hq/element-x-android/pull/6842
|
||||
* Use a raw key for the SDK stores for new sessions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6835
|
||||
* Notification settings: pick custom sounds for messages and call ringtones by @bmarty in https://github.com/element-hq/element-x-android/pull/6897
|
||||
### 🙌 Improvements
|
||||
* Do not show membership/profile events in public rooms by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6360
|
||||
* Use `runBlocking` for the token refresh logic by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6863
|
||||
* Move empty day separator filtering to a timeline post-processor by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6866
|
||||
* Add better logs to track token update failures by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6859
|
||||
* Read map tiler custom style from matrix .well-known file by @bmarty in https://github.com/element-hq/element-x-android/pull/6886
|
||||
* change(permissions) : allow to change ShareLiveLocation permission by @ganfra in https://github.com/element-hq/element-x-android/pull/6890
|
||||
* Avoid SQLCipher key derivation by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6774
|
||||
* change: replace the maplibre-compose UserLocationState to a simpler one by @ganfra in https://github.com/element-hq/element-x-android/pull/6913
|
||||
* Change : location provider by @ganfra in https://github.com/element-hq/element-x-android/pull/6935
|
||||
* Add flip actions to image edition by @bmarty in https://github.com/element-hq/element-x-android/pull/6949
|
||||
* Remove the `FloatingDateBadge` feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6950
|
||||
### 🐛 Bugfixes
|
||||
* Fix media viewer flickering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6800
|
||||
* Fix 'Conversation label cannot be empty' error by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6823
|
||||
* Release proximity wakelock on Element Call when call ends by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6825
|
||||
* Hide edit pencil from accessibility by @bmarty in https://github.com/element-hq/element-x-android/pull/6826
|
||||
* Don't compress images sent through the Files attachment picker by @cizra in https://github.com/element-hq/element-x-android/pull/6755
|
||||
* [a11y] Improve accessibility of video and audio player by @bmarty in https://github.com/element-hq/element-x-android/pull/6830
|
||||
* [a11y] Improve accessibility of screen headers. by @bmarty in https://github.com/element-hq/element-x-android/pull/6827
|
||||
* Add mark as read / unread in room details by @bmarty in https://github.com/element-hq/element-x-android/pull/6818
|
||||
* Fix formatting inconsistencies in latest event summaries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6855
|
||||
* Fix public read receipts being sent by mistake by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6838
|
||||
* Fix app having a pink top bar in the recent app list when PIN lock is setup by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6903
|
||||
* Create log messages from `WebView` just once by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6923
|
||||
* Use secondary color icon for leading icons by @bmarty in https://github.com/element-hq/element-x-android/pull/6926
|
||||
* Use reverse ordering for `FilterEmptyDayPostProcessor` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6927
|
||||
* Make Vulkan a *not required* feature by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6961
|
||||
### 🗣 Translations
|
||||
* Sync Strings - new translations to Catalan by @ElementBot in https://github.com/element-hq/element-x-android/pull/6856
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6921
|
||||
### 🧱 Build
|
||||
* Remove DI-generated code from Kover reports by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6834
|
||||
* Disable cron jobs and private SSH key jobs in forks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6821
|
||||
* Revert "Disable cron jobs and private SSH key jobs in forks" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6905
|
||||
* Downgrade detekt compose rules by @bmarty in https://github.com/element-hq/element-x-android/pull/6846
|
||||
* Add Stefan's etiquette to the project. by @bmarty in https://github.com/element-hq/element-x-android/pull/6909
|
||||
### 🚧 In development 🚧
|
||||
* [Link new device] Rotate QrCode instead of showing an error by @bmarty in https://github.com/element-hq/element-x-android/pull/6817
|
||||
* Fix image resizing behavior. by @bmarty in https://github.com/element-hq/element-x-android/pull/6924
|
||||
### Dependency upgrades
|
||||
* Update dependency net.zetetic:sqlcipher-android to v4.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6819
|
||||
* Update dependencyAnalysis to v3.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6813
|
||||
* Update metro to v1.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6832
|
||||
* Update dependency io.element.android:element-call-embedded to v0.19.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6833
|
||||
* Update kotlin to v2.3.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6822
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.20 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6831
|
||||
* Update media3 to v1.10.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6816
|
||||
* Update peaceiris/actions-gh-pages action to v4.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6820
|
||||
* Update dependencyAnalysis to v3.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6840
|
||||
* Update codecov/codecov-action action to v6.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6864
|
||||
* Update dependency io.element.android:element-call-embedded to v0.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6876
|
||||
* Update roborazzi to v1.62.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6858
|
||||
* Update dependency androidx.compose:compose-bom to v2026.05.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6888
|
||||
* Update roborazzi to v1.63.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6893
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6885
|
||||
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6891
|
||||
* Update actions/stale action to v10.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6904
|
||||
* Update tspascoal/get-user-teams-membership action to v4.0.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6900
|
||||
* Update dependency org.maplibre.gl:android-sdk to v13.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6930
|
||||
* Update dependency org.unifiedpush.android:connector to v3.3.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6937
|
||||
* Update kotlin to v2.3.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6946
|
||||
* Update dependency com.posthog:posthog-android to v3.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6920
|
||||
* Import compound token from release 10.2.1 https://github.com/element-hq/compound-design-tokens/releases/tag/v10.2.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/6942
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6947
|
||||
* Update dependency com.github.matrix-org:matrix-analytics-events to v0.36.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6939
|
||||
### Others
|
||||
* Make the avatar in the room member moderation bottom sheet clickable by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6705
|
||||
* Do not hide your own media for "Show media in timeline" by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6898
|
||||
* Report problem: add an optional TextField to provide a GitHub issue number. by @bmarty in https://github.com/element-hq/element-x-android/pull/6911
|
||||
* Fix read receipts not shown for calls by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6889
|
||||
* Reorder room detail items by @bmarty in https://github.com/element-hq/element-x-android/pull/6929
|
||||
* [Media viewer] File preview improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/6933
|
||||
* [Media bottom sheet] UI iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6938
|
||||
* Add unread count to the room unread indicator by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6887
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.05.2...v26.06.1
|
||||
|
||||
Changes in Element X v26.05.2
|
||||
=============================
|
||||
|
||||
@@ -42,6 +131,14 @@ Changes in Element X v26.05.1
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v26.05.1 -->
|
||||
|
||||
Security fixes 🔐
|
||||
-----------------
|
||||
|
||||
This release contains some important security fixes:
|
||||
|
||||
- Check the user ID in the `sender_device_keys` property of Olm-encrypted to-device events to prevent sender spoofing by homeserver owners. ([#6553](https://github.com/matrix-org/matrix-rust-sdk/pull/6553), High, [CVE-2026-45056](https://www.cve.org/CVERecord?id=CVE-2026-45056), [GHSA-wfq4-36m3-9g42](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-wfq4-36m3-9g42))
|
||||
- Reject invalid edits as candidates for timeline updates. ([#6454](https://github.com/matrix-org/matrix-rust-sdk/pull/6454), Moderate, [CVE-2026-45057](https://www.cve.org/CVERecord?id=CVE-2026-45057), [GHSA-h97m-27fx-42rx](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-h97m-27fx-42rx))
|
||||
|
||||
## What's Changed
|
||||
### ✨ Features
|
||||
* Make Element Call screen work edge-to-edge by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6634
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: added image editing before sending and an option for custom notification sounds, improved start times, several other bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: fixed an issue with devices not supporting Vulkan, added image editing before sending and an option for custom notification sounds, improved start times, several other bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
@@ -3,11 +3,12 @@
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"계정을 비활성화하시겠습니까? 이 작업은 되돌릴 수 없습니다."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"모든 내 메시지 삭제"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"경고: 향후 사용자는 불완전한 대화 내용을 볼 수 있습니다."</string>
|
||||
<string name="screen_deactivate_account_description">"계정을 비활성화하는 것은 %1$s 이며, 다음과 같은 조치를 취합니다:"</string>
|
||||
<string name="screen_deactivate_account_description">"계정을 삭제하면 %1$s, 다음과 같이 처리됩니다:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"불가역적"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s 귀하의 계정 (로그인할 수 없으며, 귀하의 ID는 재사용할 수 없습니다)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"영구적으로 비활성화"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"모든 채팅방에서 자신을 제거하세요."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"당사의 신원 서버에서 귀하의 계정 정보를 삭제하세요."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"메시지는 등록된 사용자에게는 계속 표시되지만, 삭제하면 신규 또는 미등록 사용자는 볼 수 없게 됩니다."</string>
|
||||
<string name="screen_deactivate_account_title">"계정 삭제"</string>
|
||||
</resources>
|
||||
|
||||
+1
@@ -279,6 +279,7 @@ private fun RoomsViewList(
|
||||
hideInviteAvatars = hideInvitesAvatars,
|
||||
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
|
||||
state.seenRoomInvites.contains(room.roomId),
|
||||
showUnreadCount = state.showUnreadCount,
|
||||
onClick = onRoomClick,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+14
-2
@@ -76,8 +76,9 @@ internal fun RoomSummaryRow(
|
||||
hideInviteAvatars: Boolean,
|
||||
isInviteSeen: Boolean,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showUnreadCount: Boolean = false,
|
||||
eventSink: (RoomListEvent) -> Unit,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
when (room.displayType) {
|
||||
@@ -127,7 +128,7 @@ internal fun RoomSummaryRow(
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
MessagePreviewAndIndicatorRow(room = room)
|
||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.KNOCKED -> {
|
||||
@@ -273,6 +274,7 @@ private fun InviteSubtitle(
|
||||
@Composable
|
||||
private fun MessagePreviewAndIndicatorRow(
|
||||
room: RoomListRoomSummary,
|
||||
showUnreadCount: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -360,8 +362,18 @@ private fun MessagePreviewAndIndicatorRow(
|
||||
}
|
||||
if (room.hasNewContent) {
|
||||
val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
|
||||
val count = if (showUnreadCount) {
|
||||
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
||||
room.numberOfUnreadMessages
|
||||
} else {
|
||||
room.numberOfUnreadNotifications
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
UnreadIndicatorAtom(
|
||||
color = tint,
|
||||
count = count,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -36,6 +36,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
||||
internal fun aRoomsContentState(
|
||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||
showNewNotificationSoundBanner: Boolean = false,
|
||||
showUnreadCount: Boolean = false,
|
||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
||||
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
|
||||
@@ -43,6 +44,7 @@ internal fun aRoomsContentState(
|
||||
) = RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||
showUnreadCount = showUnreadCount,
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
||||
batteryOptimizationState = batteryOptimizationState,
|
||||
summaries = summaries,
|
||||
|
||||
+11
-2
@@ -44,6 +44,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -90,6 +92,7 @@ class RoomListPresenter(
|
||||
private val announcementService: AnnouncementService,
|
||||
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService = client.encryptionService
|
||||
|
||||
@@ -165,13 +168,17 @@ class RoomListPresenter(
|
||||
roomListDataSource.updateFilter(allFilters)
|
||||
}
|
||||
|
||||
val canReportRoom by produceState(false) { value = client.canReportRoom() }
|
||||
val showUnreadCount by produceState(false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount)
|
||||
}
|
||||
|
||||
val contentState = roomListContentState(
|
||||
securityBannerDismissed,
|
||||
showNewNotificationSoundBanner,
|
||||
showUnreadCount,
|
||||
)
|
||||
|
||||
val canReportRoom by produceState(false) { value = client.canReportRoom() }
|
||||
|
||||
return RoomListState(
|
||||
contextMenu = contextMenu.value,
|
||||
declineInviteMenu = declineInviteMenu.value,
|
||||
@@ -226,6 +233,7 @@ class RoomListPresenter(
|
||||
private fun roomListContentState(
|
||||
securityBannerDismissed: Boolean,
|
||||
showNewNotificationSoundBanner: Boolean,
|
||||
showUnreadCount: Boolean,
|
||||
): RoomListContentState {
|
||||
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
|
||||
roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) }
|
||||
@@ -254,6 +262,7 @@ class RoomListPresenter(
|
||||
RoomListContentState.Rooms(
|
||||
securityBannerState = securityBannerState,
|
||||
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
|
||||
showUnreadCount = showUnreadCount,
|
||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
||||
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
||||
summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ sealed interface RoomListContentState {
|
||||
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
||||
val batteryOptimizationState: BatteryOptimizationState,
|
||||
val showNewNotificationSoundBanner: Boolean,
|
||||
val showUnreadCount: Boolean,
|
||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||
val seenRoomInvites: ImmutableSet<RoomId>,
|
||||
) : RoomListContentState
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_battery_optimization_content_android">"すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。"</string>
|
||||
<string name="banner_battery_optimization_submit_android">"最適化を無効にする"</string>
|
||||
<string name="banner_battery_optimization_submit_android">"無効にする"</string>
|
||||
<string name="banner_battery_optimization_title_android">"通知が届いていませんか?"</string>
|
||||
<string name="banner_new_sound_message">"通知音が更新され、より明確で速く、そして邪魔にならなくなりました。"</string>
|
||||
<string name="banner_new_sound_title">"サウンドを刷新しました"</string>
|
||||
@@ -38,7 +38,7 @@
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"低い優先度のチャットはまだありません"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"フィルターを解除して他のチャットを表示できます"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"この選択中にチャットがありません"</string>
|
||||
<string name="screen_roomlist_filter_people">"人々"</string>
|
||||
<string name="screen_roomlist_filter_people">"ユーザー"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"まだダイレクトメッセージは届いていません"</string>
|
||||
<string name="screen_roomlist_filter_rooms">"ルーム"</string>
|
||||
<string name="screen_roomlist_filter_rooms_empty_state_title">"まだルームに参加していません"</string>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<string name="banner_new_sound_message">"Vaše oznámenia boli aktualizované – sú prehľadnejšie, rýchlejšie a menej rušivé."</string>
|
||||
<string name="banner_new_sound_title">"Obnovili sme vaše zvuky"</string>
|
||||
<string name="banner_set_up_recovery_content">"Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Nastaviť obnovenie"</string>
|
||||
<string name="banner_set_up_recovery_submit">"Získať kľúč na obnovenie"</string>
|
||||
<string name="banner_set_up_recovery_title">"Nastaviť obnovenie"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"Zadajte kľúč na obnovenie"</string>
|
||||
|
||||
+4
@@ -36,6 +36,8 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -669,6 +671,7 @@ class RoomListPresenterTest {
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
leaveRoomPresenter = { leaveRoomState },
|
||||
@@ -697,5 +700,6 @@ class RoomListPresenterTest {
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
announcementService = announcementService,
|
||||
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invite_users_already_a_member">"이미 회원"</string>
|
||||
<string name="screen_invite_users_already_invited">"이미 초대됨"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"이 사용자들과의 대화 내역이 없습니다. 계속하려면 먼저 이 방으로의 초대를 확인해 주세요."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"이 사용자와의 대화 내역이 없습니다. 계속하려면 먼저 이 방으로의 초대를 확인해 주세요."</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"이 방에 새 사용자를 초대할까요?"</string>
|
||||
<string name="screen_invite_users_confirm_dialog_title_one_user">"이 방에 새 사용자를 초대할까요?"</string>
|
||||
</resources>
|
||||
|
||||
+6
-6
@@ -73,10 +73,10 @@ fun ErrorView(
|
||||
|
||||
@Composable
|
||||
private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when (errorScreenType) {
|
||||
ErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_title)
|
||||
ErrorScreenType.Cancelled -> stringResource(R.string.screen_link_new_device_error_request_cancelled_title)
|
||||
ErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_title)
|
||||
ErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_title)
|
||||
ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_title)
|
||||
ErrorScreenType.Expired -> stringResource(R.string.screen_link_new_device_error_request_timeout_title)
|
||||
ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_link_new_device_error_not_supported_title)
|
||||
ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_title)
|
||||
ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_title)
|
||||
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName)
|
||||
@@ -86,10 +86,10 @@ private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when
|
||||
|
||||
@Composable
|
||||
private fun subtitleText(errorScreenType: ErrorScreenType, appName: String) = when (errorScreenType) {
|
||||
ErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_subtitle)
|
||||
ErrorScreenType.Cancelled -> stringResource(R.string.screen_link_new_device_error_request_cancelled_subtitle)
|
||||
ErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_subtitle)
|
||||
ErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_subtitle)
|
||||
ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_subtitle, appName)
|
||||
ErrorScreenType.Expired -> stringResource(R.string.screen_link_new_device_error_request_timeout_subtitle)
|
||||
ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_link_new_device_error_not_supported_subtitle)
|
||||
ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_subtitle)
|
||||
ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description)
|
||||
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName)
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun RenderingMapsNotSupportedDialog(onSubmit: () -> Unit) {
|
||||
ErrorDialog(
|
||||
title = stringResource(CommonStrings.vulkan_not_supported_dialog_title_android),
|
||||
content = stringResource(CommonStrings.vulkan_not_supported_dialog_content_android),
|
||||
onSubmit = onSubmit,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RenderingMapsNotSupportedDialogPreview() = ElementPreview {
|
||||
RenderingMapsNotSupportedDialog(onSubmit = {})
|
||||
}
|
||||
@@ -5,13 +5,21 @@
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
~ Please see LICENSE files in the repository root for full details.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- Declare Vulkan as an optional feature to prevent installation issues on devices that do not support it. -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.vulkan.version"
|
||||
android:required="false"
|
||||
tools:replace="android:required" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".live.service.LiveLocationSharingService"
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.rememberAndroidLocationProvider
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class UserLocationState(locationState: State<Location?>) {
|
||||
val location: Location? by locationState
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState {
|
||||
val isPreview = LocalInspectionMode.current
|
||||
val locationProvider = if (isPreview || !hasLocationPermission) {
|
||||
rememberNullLocationProvider()
|
||||
} else {
|
||||
rememberAndroidLocationProvider(
|
||||
updateInterval = 5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.High,
|
||||
minDistance = 5.meters,
|
||||
)
|
||||
}
|
||||
val locationState = locationProvider.location.collectAsState()
|
||||
return remember { UserLocationState(locationState) }
|
||||
}
|
||||
+18
-36
@@ -10,9 +10,8 @@ package io.element.android.features.location.impl.common.ui
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.UserLocationState
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.LocationPuck
|
||||
import org.maplibre.compose.location.LocationPuckColors
|
||||
import org.maplibre.compose.location.LocationPuckSizes
|
||||
@@ -20,40 +19,23 @@ import org.maplibre.compose.location.LocationPuckSizes
|
||||
@Composable
|
||||
fun UserLocationPuck(
|
||||
cameraState: CameraState,
|
||||
locationState: UserLocationState,
|
||||
trackUserLocation: Boolean,
|
||||
location: Location?,
|
||||
) {
|
||||
SimpleLocationTrackingEffect(
|
||||
locationState = locationState,
|
||||
enabled = trackUserLocation,
|
||||
) { currentLocation ->
|
||||
val target = currentLocation?.position?.value ?: cameraState.position.target
|
||||
val newPosition = cameraState.position.copy(
|
||||
target = target,
|
||||
// Force pointing to NORTH
|
||||
bearing = 0.0,
|
||||
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
||||
LocationPuck(
|
||||
idPrefix = "user-location",
|
||||
location = location,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||
showBearingAccuracy = false,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(
|
||||
dotRadius = 8.dp,
|
||||
dotStrokeWidth = 2.dp,
|
||||
),
|
||||
colors = LocationPuckColors(
|
||||
dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary,
|
||||
dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary,
|
||||
dotStrokeColor = ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
cameraState.animateTo(newPosition)
|
||||
}
|
||||
val location = locationState.location
|
||||
if (location != null) {
|
||||
LocationPuck(
|
||||
idPrefix = "user-location",
|
||||
location = location,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||
showBearingAccuracy = false,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(
|
||||
dotRadius = 8.dp,
|
||||
dotStrokeWidth = 2.dp,
|
||||
),
|
||||
colors = LocationPuckColors(
|
||||
dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary,
|
||||
dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary,
|
||||
dotStrokeColor = ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultUserLocationStateFactory : UserLocationState.Factory {
|
||||
@Composable
|
||||
override fun create(hasLocationPermission: Boolean): UserLocationState {
|
||||
val locationProvider = if (!hasLocationPermission) {
|
||||
rememberNullLocationProvider()
|
||||
} else {
|
||||
@SuppressLint("MissingPermission")
|
||||
rememberPlatformLocationProvider(
|
||||
updateInterval = 5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.High,
|
||||
minDistance = 5.meters,
|
||||
)
|
||||
}
|
||||
val location by locationProvider.location.collectAsState()
|
||||
return UserLocationState(location)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
import org.maplibre.compose.location.PositionWithAccuracy
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import kotlin.time.TimeSource
|
||||
import org.maplibre.compose.location.Location as MapLibreLocation
|
||||
|
||||
fun Location.asMapLibreLocation(): MapLibreLocation {
|
||||
return MapLibreLocation(
|
||||
position = PositionWithAccuracy(
|
||||
value = Position(latitude = lat, longitude = lon),
|
||||
accuracy = accuracy?.toDouble()?.meters
|
||||
),
|
||||
// Not relevant as not used
|
||||
timestamp = TimeSource.Monotonic.markNow(),
|
||||
)
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import androidx.core.os.ExecutorCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.LocationProvider
|
||||
import org.maplibre.compose.location.PermissionException
|
||||
import org.maplibre.compose.location.asMapLibreLocation
|
||||
import org.maplibre.spatialk.units.Length
|
||||
import org.maplibre.spatialk.units.extensions.inMeters
|
||||
import kotlin.time.Duration
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class PlatformLocationProvider(
|
||||
context: Context,
|
||||
private val updateInterval: Duration,
|
||||
private val minDistance: Length,
|
||||
private val desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High,
|
||||
coroutineScope: CoroutineScope,
|
||||
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
|
||||
) : LocationProvider {
|
||||
override val location: StateFlow<Location?>
|
||||
|
||||
init {
|
||||
if (!handlerThread.isAlive) handlerThread.start()
|
||||
if (
|
||||
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
||||
context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
throw PermissionException()
|
||||
}
|
||||
val locationManager = context.getSystemService(LocationManager::class.java)
|
||||
val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) }
|
||||
val locationFlow = if (provider != null) {
|
||||
createProviderFlow(locationManager, provider)
|
||||
} else {
|
||||
emptyFlow()
|
||||
}
|
||||
location = locationFlow.stateIn(coroutineScope, sharingStarted, null)
|
||||
}
|
||||
|
||||
@RequiresPermission(
|
||||
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
|
||||
)
|
||||
private fun createProviderFlow(locationManager: LocationManager, provider: String) = callbackFlow {
|
||||
send(locationManager.getLastKnownLocation(provider)?.asMapLibreLocation())
|
||||
val listener = LocationListenerCompat { trySend(it.asMapLibreLocation()) }
|
||||
val request = LocationRequestCompat.Builder(updateInterval.inWholeMilliseconds)
|
||||
.setQuality(desiredAccuracy.toLocationRequestQuality())
|
||||
.setMinUpdateDistanceMeters(minDistance.inMeters.toFloat())
|
||||
.build()
|
||||
LocationManagerCompat.requestLocationUpdates(
|
||||
locationManager,
|
||||
provider,
|
||||
request,
|
||||
ExecutorCompat.create(Handler(handlerThread.looper)),
|
||||
listener,
|
||||
)
|
||||
awaitClose { LocationManagerCompat.removeUpdates(locationManager, listener) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val PROVIDERS_BY_PRIORITY = listOf(
|
||||
LocationManager.FUSED_PROVIDER,
|
||||
LocationManager.GPS_PROVIDER,
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
)
|
||||
private val handlerThread by lazy { HandlerThread("PlatformLocationProvider") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun DesiredAccuracy.toLocationRequestQuality(): Int = when (this) {
|
||||
DesiredAccuracy.Highest, DesiredAccuracy.High -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
|
||||
DesiredAccuracy.Balanced -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
|
||||
DesiredAccuracy.Low, DesiredAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER
|
||||
}
|
||||
|
||||
@Composable
|
||||
@RequiresPermission(
|
||||
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
|
||||
)
|
||||
fun rememberPlatformLocationProvider(
|
||||
updateInterval: Duration,
|
||||
minDistance: Length,
|
||||
desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High,
|
||||
context: Context = LocalContext.current,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
|
||||
): PlatformLocationProvider {
|
||||
return remember(context, updateInterval, minDistance, desiredAccuracy, coroutineScope, sharingStarted) {
|
||||
PlatformLocationProvider(
|
||||
context = context,
|
||||
updateInterval = updateInterval,
|
||||
minDistance = minDistance,
|
||||
desiredAccuracy = desiredAccuracy,
|
||||
coroutineScope = coroutineScope,
|
||||
sharingStarted = sharingStarted,
|
||||
)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.maplibre.compose.location.Location
|
||||
|
||||
data class UserLocationState(val location: Location?) {
|
||||
fun interface Factory {
|
||||
@Composable
|
||||
fun create(hasLocationPermission: Boolean): UserLocationState
|
||||
}
|
||||
}
|
||||
+31
-5
@@ -5,15 +5,16 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.ui
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import io.element.android.features.location.impl.common.UserLocationState
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.location.Location
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -22,16 +23,17 @@ import kotlin.math.abs
|
||||
* TODO remove once https://github.com/maplibre/maplibre-compose/issues/808 is fixed
|
||||
*/
|
||||
@Composable
|
||||
internal fun SimpleLocationTrackingEffect(
|
||||
internal fun UserLocationTrackingEffect(
|
||||
locationState: UserLocationState,
|
||||
enabled: Boolean = true,
|
||||
precision: Double = 0.00001,
|
||||
onLocationChange: suspend (Location?) -> Unit,
|
||||
) {
|
||||
val latestOnLocationChange by rememberUpdatedState(onLocationChange)
|
||||
LaunchedEffect(locationState, enabled) {
|
||||
val latestLocationState by rememberUpdatedState(locationState)
|
||||
LaunchedEffect(enabled) {
|
||||
if (!enabled) return@LaunchedEffect
|
||||
val locationStateFlow = snapshotFlow { locationState.location }
|
||||
val locationStateFlow = snapshotFlow { latestLocationState.location }
|
||||
locationStateFlow
|
||||
.distinctUntilChanged { oldLocation, newLocation ->
|
||||
if (oldLocation != null && newLocation != null) {
|
||||
@@ -49,3 +51,27 @@ internal fun SimpleLocationTrackingEffect(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun UserLocationTrackingEffect(
|
||||
cameraState: CameraState,
|
||||
locationState: UserLocationState,
|
||||
enabled: Boolean = true,
|
||||
precision: Double = 0.00001,
|
||||
) {
|
||||
UserLocationTrackingEffect(
|
||||
locationState = locationState,
|
||||
enabled = enabled,
|
||||
precision = precision
|
||||
) { location ->
|
||||
val target = location?.position?.value ?: cameraState.position.target
|
||||
cameraState.animateTo(
|
||||
cameraState.position.copy(
|
||||
target = target,
|
||||
// Force pointing to NORTH
|
||||
bearing = 0.0,
|
||||
zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
@@ -133,6 +133,11 @@ class DefaultActiveLiveLocationShareManager(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onUnrecoverableError() {
|
||||
Timber.d("ActiveLiveLocationShareManager unrecoverable error, stopping all shares")
|
||||
localSharingRoomIds.value.toList().forEach { stopShare(it) }
|
||||
}
|
||||
|
||||
override suspend fun onLocationUpdate(location: Location) {
|
||||
val activeSharesCount = localSharingRoomIds.value.size
|
||||
Timber.d("ActiveLiveLocationShareManager received location update for $activeSharesCount active share(s)")
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ package io.element.android.features.location.impl.live.service
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
|
||||
fun interface LiveLocationReceiver {
|
||||
interface LiveLocationReceiver {
|
||||
suspend fun onLocationUpdate(location: Location)
|
||||
suspend fun onUnrecoverableError()
|
||||
}
|
||||
|
||||
+11
@@ -77,6 +77,17 @@ class LiveLocationSharingCoordinator internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun dispatchUnrecoverableError() {
|
||||
Timber.d("LiveLocationSharingCoordinator dispatching unrecoverable error")
|
||||
receivers.forEach { (sessionId, receiver) ->
|
||||
runCatchingExceptions {
|
||||
receiver.onUnrecoverableError()
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to dispatch unrecoverable error for session $sessionId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun dispatch(location: Location) {
|
||||
val currentTimeMillis = nowMillis()
|
||||
val millisSincePrevious = currentTimeMillis - lastDispatchMillis.load()
|
||||
|
||||
+19
-11
@@ -14,6 +14,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.location.impl.common.userlocation.PlatformLocationProvider
|
||||
import io.element.android.features.location.impl.di.LocationBindings
|
||||
import io.element.android.features.location.impl.live.notification.LiveLocationSharingNotificationCreator
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
@@ -29,13 +30,15 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.maplibre.compose.location.AndroidLocationProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.PermissionException
|
||||
import org.maplibre.spatialk.units.extensions.inMeters
|
||||
import org.maplibre.spatialk.units.extensions.meters
|
||||
import timber.log.Timber
|
||||
@@ -48,7 +51,6 @@ class LiveLocationSharingService : Service() {
|
||||
@Inject lateinit var coordinator: LiveLocationSharingCoordinator
|
||||
@Inject lateinit var notificationCreator: LiveLocationSharingNotificationCreator
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
|
||||
@Inject lateinit var appForegroundStateService: AppForegroundStateService
|
||||
|
||||
@AppCoroutineScope
|
||||
@@ -62,8 +64,8 @@ class LiveLocationSharingService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.d("LiveLocationSharingService onCreate")
|
||||
bindings<LocationBindings>().inject(this)
|
||||
runCatchingExceptions {
|
||||
bindings<LocationBindings>().inject(this)
|
||||
appForegroundStateService.updateIsSharingLiveLocation(true)
|
||||
coroutineScope = appCoroutineScope.childScope(Dispatchers.Default, "LiveLocationSharingService")
|
||||
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.LIVE_LOCATION)
|
||||
@@ -81,6 +83,7 @@ class LiveLocationSharingService : Service() {
|
||||
startLocationUpdatesListener()
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to start live location sharing service")
|
||||
appCoroutineScope.launch { coordinator.dispatchUnrecoverableError() }
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
@@ -90,14 +93,19 @@ class LiveLocationSharingService : Service() {
|
||||
Timber.d("LiveLocationSharingService listening to location updates")
|
||||
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
|
||||
.flatMapLatest { minDistanceMeters ->
|
||||
val locationProvider = AndroidLocationProvider(
|
||||
context = applicationContext,
|
||||
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
||||
minDistance = minDistanceMeters.meters,
|
||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
locationProvider.location
|
||||
try {
|
||||
PlatformLocationProvider(
|
||||
context = applicationContext,
|
||||
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
||||
minDistance = minDistanceMeters.meters,
|
||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||
coroutineScope = coroutineScope
|
||||
).location
|
||||
} catch (exception: PermissionException) {
|
||||
Timber.e(exception, "Failed to create PlatformLocationProvider")
|
||||
coordinator.dispatchUnrecoverableError()
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.map { location ->
|
||||
|
||||
+5
-1
@@ -33,6 +33,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.sendLiveLocationPermissions
|
||||
import io.element.android.features.location.impl.common.toDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -70,6 +71,7 @@ class ShareLocationPresenter(
|
||||
private val durationFormatter: DurationFormatter,
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val liveLocationStore: LiveLocationStore,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
) : Presenter<ShareLocationState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@@ -123,6 +125,8 @@ class ShareLocationPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted)
|
||||
|
||||
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() }
|
||||
|
||||
fun handleEvent(event: ShareLocationEvent) {
|
||||
@@ -171,7 +175,7 @@ class ShareLocationPresenter(
|
||||
currentUser = currentUser,
|
||||
dialogState = dialogState,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
userLocationState = userLocationState,
|
||||
canShareLiveLocation = timelineMode.canShareLiveLocation(),
|
||||
appName = appName,
|
||||
startLiveLocationAction = startLiveLocationAction.value,
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.location.impl.share
|
||||
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
@@ -19,7 +20,7 @@ data class ShareLocationState(
|
||||
val currentUser: MatrixUser,
|
||||
val dialogState: Dialog,
|
||||
val trackUserLocation: Boolean,
|
||||
val hasLocationPermission: Boolean,
|
||||
val userLocationState: UserLocationState,
|
||||
val appName: String,
|
||||
val canShareLiveLocation: Boolean,
|
||||
val startLiveLocationAction: AsyncAction<Unit>,
|
||||
|
||||
+3
-11
@@ -10,6 +10,7 @@ package io.element.android.features.location.impl.share
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -26,43 +27,35 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.None,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
@@ -74,7 +67,6 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
)
|
||||
),
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
@@ -88,7 +80,7 @@ fun aShareLocationState(
|
||||
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
||||
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
|
||||
trackUserPosition: Boolean = false,
|
||||
hasLocationPermission: Boolean = false,
|
||||
userLocationState: UserLocationState = UserLocationState(null),
|
||||
canShareLiveLocation: Boolean = false,
|
||||
appName: String = APP_NAME,
|
||||
startLiveLocationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
@@ -99,7 +91,7 @@ fun aShareLocationState(
|
||||
currentUser = currentUser,
|
||||
dialogState = dialogState,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = hasLocationPermission,
|
||||
userLocationState = userLocationState,
|
||||
canShareLiveLocation = canShareLiveLocation,
|
||||
appName = appName,
|
||||
startLiveLocationAction = startLiveLocationAction,
|
||||
|
||||
+8
-8
@@ -39,12 +39,11 @@ import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
import io.element.android.features.location.impl.R
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.UserLocationState
|
||||
import io.element.android.features.location.impl.common.rememberUserLocationState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect
|
||||
import io.element.android.features.location.impl.share.ShareLocationEvent.StartLiveLocationShare
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.LocationPin
|
||||
@@ -109,7 +108,6 @@ fun ShareLocationView(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
|
||||
)
|
||||
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
|
||||
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
||||
|
||||
LaunchedEffect(cameraState.isCameraMoving) {
|
||||
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
||||
@@ -136,15 +134,18 @@ fun ShareLocationView(
|
||||
BottomSheetContent(
|
||||
cameraState = cameraState,
|
||||
state = state,
|
||||
userLocationState = userLocationState,
|
||||
navigateUp = navigateUp
|
||||
)
|
||||
},
|
||||
mapContent = {
|
||||
UserLocationTrackingEffect(
|
||||
cameraState = cameraState,
|
||||
locationState = state.userLocationState,
|
||||
enabled = state.trackUserLocation,
|
||||
)
|
||||
UserLocationPuck(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.trackUserLocation
|
||||
location = state.userLocationState.location,
|
||||
)
|
||||
},
|
||||
overlayContent = { sheetPadding ->
|
||||
@@ -215,11 +216,10 @@ private fun StartLiveLocationActionView(
|
||||
private fun BottomSheetContent(
|
||||
cameraState: CameraState,
|
||||
state: ShareLocationState,
|
||||
userLocationState: UserLocationState,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
val userLocation = userLocationState.location
|
||||
val userLocation = state.userLocationState.location
|
||||
if (state.trackUserLocation && userLocation != null) {
|
||||
ShareCurrentLocationItem {
|
||||
state.eventSink(
|
||||
|
||||
+54
-39
@@ -10,12 +10,14 @@ package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
@@ -23,6 +25,7 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||
import io.element.android.features.location.api.live.isCurrentlySharing
|
||||
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.SendLiveLocationPermissions
|
||||
@@ -33,6 +36,8 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.toDialogState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.features.location.impl.common.userlocation.asMapLibreLocation
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
@@ -52,8 +57,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class ShowLocationPresenter(
|
||||
@AssistedInject class ShowLocationPresenter(
|
||||
@Assisted private val mode: ShowLocationMode,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val locationActions: LocationActions,
|
||||
@@ -63,9 +67,9 @@ class ShowLocationPresenter(
|
||||
private val client: MatrixClient,
|
||||
private val joinedRoom: JoinedRoom,
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
) : Presenter<ShowLocationState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@AssistedFactory fun interface Factory {
|
||||
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
||||
}
|
||||
|
||||
@@ -155,55 +159,66 @@ class ShowLocationPresenter(
|
||||
val liveLocationSharesFlow = joinedRoom.subscribeToLiveLocationShares()
|
||||
val membersStateFlow = joinedRoom.membersStateFlow.mapState { it.joinedRoomMembers() }
|
||||
combine(liveLocationSharesFlow, membersStateFlow) { liveShares, members ->
|
||||
liveShares
|
||||
.sortedWith(comparator)
|
||||
.mapNotNull { share ->
|
||||
val lastLocation = share.lastLocation ?: return@mapNotNull null
|
||||
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
|
||||
val member = members.find { it.userId == share.userId }
|
||||
val displayName = member?.getBestName() ?: share.userId.value
|
||||
val avatarUrl = member?.avatarUrl
|
||||
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
|
||||
val formattedTimestamp = stringProvider.getString(
|
||||
CommonStrings.screen_static_location_sheet_timestamp_description,
|
||||
relativeTime
|
||||
)
|
||||
LocationShareItem(
|
||||
userId = share.userId,
|
||||
displayName = displayName,
|
||||
avatarData = AvatarData(
|
||||
id = share.userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.UserListItem,
|
||||
),
|
||||
formattedTimestamp = formattedTimestamp,
|
||||
location = location,
|
||||
isLive = true,
|
||||
assetType = lastLocation.assetType,
|
||||
isOwnUser = share.userId == joinedRoom.sessionId
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
liveShares.sortedWith(comparator).mapNotNull { share ->
|
||||
val lastLocation = share.lastLocation ?: return@mapNotNull null
|
||||
val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null
|
||||
val member = members.find { it.userId == share.userId }
|
||||
val displayName = member?.getBestName() ?: share.userId.value
|
||||
val avatarUrl = member?.avatarUrl
|
||||
val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true)
|
||||
val formattedTimestamp = stringProvider.getString(
|
||||
CommonStrings.screen_static_location_sheet_timestamp_description,
|
||||
relativeTime
|
||||
)
|
||||
LocationShareItem(
|
||||
userId = share.userId,
|
||||
displayName = displayName,
|
||||
avatarData = AvatarData(
|
||||
id = share.userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.UserListItem,
|
||||
),
|
||||
formattedTimestamp = formattedTimestamp,
|
||||
location = location,
|
||||
isLive = true,
|
||||
assetType = lastLocation.assetType,
|
||||
isOwnUser = share.userId == joinedRoom.sessionId
|
||||
)
|
||||
}.toImmutableList()
|
||||
}.collect { value = it }
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
val focusedLocation = when (mode) {
|
||||
is ShowLocationMode.Static -> locationShares.firstOrNull()
|
||||
is ShowLocationMode.Live -> locationShares.firstOrNull { it.userId == mode.senderId }
|
||||
val updatedLocationShares by rememberUpdatedState(locationShares)
|
||||
val focusedLocation by remember {
|
||||
derivedStateOf {
|
||||
when (mode) {
|
||||
is ShowLocationMode.Static -> updatedLocationShares.firstOrNull()
|
||||
is ShowLocationMode.Live -> updatedLocationShares.firstOrNull { it.userId == mode.senderId }
|
||||
}
|
||||
}
|
||||
}
|
||||
val isCurrentlySharing by liveLocationShareManager.isCurrentlySharing(roomId = joinedRoom.roomId).collectAsState()
|
||||
val hideUserLocationPuck = mode is ShowLocationMode.Live && isCurrentlySharing
|
||||
val userLocationState = if (hideUserLocationPuck) {
|
||||
// When sharing with this device, use the user LocationShareItem as source of data instead of the device.
|
||||
val ownLocationShare by remember { derivedStateOf { updatedLocationShares.find { it.isOwnUser }?.location?.asMapLibreLocation() } }
|
||||
UserLocationState(ownLocationShare)
|
||||
} else {
|
||||
userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted)
|
||||
}
|
||||
|
||||
return ShowLocationState(
|
||||
customMapStyleUrl = customMapStyleUrl,
|
||||
dialogState = dialogState,
|
||||
locationShares = locationShares,
|
||||
focusedLocation = focusedLocation,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
userLocationState = userLocationState,
|
||||
isLive = mode is ShowLocationMode.Live,
|
||||
appName = appName,
|
||||
hideUserLocationPuck = hideUserLocationPuck,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
@@ -24,9 +25,10 @@ data class ShowLocationState(
|
||||
val dialogState: LocationConstraintsDialogState,
|
||||
val locationShares: ImmutableList<LocationShareItem>,
|
||||
val focusedLocation: LocationShareItem?,
|
||||
val hasLocationPermission: Boolean,
|
||||
val isTrackMyLocation: Boolean,
|
||||
val userLocationState: UserLocationState,
|
||||
val appName: String,
|
||||
val hideUserLocationPuck: Boolean,
|
||||
val eventSink: (ShowLocationEvent) -> Unit,
|
||||
) {
|
||||
val isSheetDraggable = isLive && locationShares.isNotEmpty()
|
||||
|
||||
+7
-13
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -33,18 +34,9 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||
),
|
||||
aShowLocationState(
|
||||
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = true,
|
||||
),
|
||||
aShowLocationState(
|
||||
customMapStyleUrl = AsyncData.Loading(),
|
||||
),
|
||||
aShowLocationState(isTrackMyLocation = true),
|
||||
aShowLocationState(customMapStyleUrl = AsyncData.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,9 +48,10 @@ fun aShowLocationState(
|
||||
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
|
||||
locationShares: List<LocationShareItem> = listOf(aLocationShareItem(isLive = isLive)),
|
||||
focusedLocation: LocationShareItem? = locationShares.firstOrNull(),
|
||||
hasLocationPermission: Boolean = false,
|
||||
isTrackMyLocation: Boolean = false,
|
||||
userLocationState: UserLocationState = UserLocationState(null),
|
||||
appName: String = APP_NAME,
|
||||
hideUserLocationPuck: Boolean = false,
|
||||
eventSink: (ShowLocationEvent) -> Unit = {},
|
||||
): ShowLocationState {
|
||||
return ShowLocationState(
|
||||
@@ -66,8 +59,9 @@ fun aShowLocationState(
|
||||
dialogState = constraintsDialogState,
|
||||
locationShares = locationShares.toImmutableList(),
|
||||
focusedLocation = focusedLocation,
|
||||
hasLocationPermission = hasLocationPermission,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
userLocationState = userLocationState,
|
||||
hideUserLocationPuck = hideUserLocationPuck,
|
||||
appName = appName,
|
||||
isLive = isLive,
|
||||
eventSink = eventSink,
|
||||
|
||||
+10
-5
@@ -37,13 +37,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.rememberUserLocationState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.LocationPinMarkers
|
||||
import io.element.android.features.location.impl.common.ui.LocationShareRow
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -91,7 +91,6 @@ fun ShowLocationView(
|
||||
}
|
||||
}
|
||||
|
||||
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded)
|
||||
)
|
||||
@@ -169,11 +168,17 @@ fun ShowLocationView(
|
||||
}
|
||||
},
|
||||
mapContent = {
|
||||
UserLocationPuck(
|
||||
UserLocationTrackingEffect(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.isTrackMyLocation
|
||||
locationState = state.userLocationState,
|
||||
enabled = state.isTrackMyLocation,
|
||||
)
|
||||
if (!state.hideUserLocationPuck) {
|
||||
UserLocationPuck(
|
||||
cameraState = cameraState,
|
||||
location = state.userLocationState.location,
|
||||
)
|
||||
}
|
||||
val markers = remember(state.locationShares) {
|
||||
state.locationShares.map { it.toMarkerData() }.toImmutableList()
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_share_location_live_location_disclaimer_title">"실시간 위치 기록은 대화방에 저장되며, 공유 종료 후에도 멤버들이 확인할 수 있습니다."</string>
|
||||
<string name="screen_share_location_live_location_duration_picker_title">"실시간 위치를 공유할 시간을 선택해 주세요."</string>
|
||||
<string name="screen_share_location_live_location_missing_permissions">"이 방에서는 실시간 위치를 공유할 권한이 없습니다."</string>
|
||||
</resources>
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
|
||||
class FakeUserLocationStateFactory : UserLocationState.Factory {
|
||||
@Composable
|
||||
override fun create(hasLocationPermission: Boolean): UserLocationState {
|
||||
return UserLocationState(null)
|
||||
}
|
||||
}
|
||||
+13
-6
@@ -27,7 +27,7 @@ class LiveLocationSharingCoordinatorTest {
|
||||
nowMillis = { 0L },
|
||||
)
|
||||
|
||||
coordinator.register(A_SESSION_ID, LiveLocationReceiver { })
|
||||
coordinator.register(A_SESSION_ID, liveLocationReceiver())
|
||||
coordinator.unregister(A_SESSION_ID)
|
||||
|
||||
assertThat(startCount).isEqualTo(1)
|
||||
@@ -43,8 +43,8 @@ class LiveLocationSharingCoordinatorTest {
|
||||
nowMillis = { 4_000L },
|
||||
)
|
||||
|
||||
coordinator.register(A_SESSION_ID) { error("boom") }
|
||||
coordinator.register(A_SESSION_ID_2) { location -> delivered += location }
|
||||
coordinator.register(A_SESSION_ID, liveLocationReceiver { error("boom") })
|
||||
coordinator.register(A_SESSION_ID_2, liveLocationReceiver { delivered += it })
|
||||
coordinator.dispatch(Location(lat = 1.0, lon = 2.0, accuracy = 3f))
|
||||
|
||||
assertThat(delivered).containsExactly(Location(lat = 1.0, lon = 2.0, accuracy = 3f))
|
||||
@@ -60,7 +60,7 @@ class LiveLocationSharingCoordinatorTest {
|
||||
nowMillis = { nowMillis },
|
||||
)
|
||||
|
||||
coordinator.register(A_SESSION_ID) { location -> delivered += location }
|
||||
coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it })
|
||||
|
||||
val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f)
|
||||
|
||||
@@ -79,7 +79,7 @@ class LiveLocationSharingCoordinatorTest {
|
||||
nowMillis = { nowMillis },
|
||||
)
|
||||
|
||||
coordinator.register(A_SESSION_ID) { location -> delivered += location }
|
||||
coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it })
|
||||
|
||||
val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f)
|
||||
val secondLocation = Location(lat = 4.0, lon = 5.0, accuracy = 6f)
|
||||
@@ -101,7 +101,7 @@ class LiveLocationSharingCoordinatorTest {
|
||||
nowMillis = { nowMillis },
|
||||
)
|
||||
|
||||
coordinator.register(A_SESSION_ID) { location -> delivered += location }
|
||||
coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it })
|
||||
|
||||
val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f)
|
||||
val secondLocation = Location(lat = 4.0, lon = 5.0, accuracy = 6f)
|
||||
@@ -113,3 +113,10 @@ class LiveLocationSharingCoordinatorTest {
|
||||
assertThat(delivered).containsExactly(firstLocation, secondLocation).inOrder()
|
||||
}
|
||||
}
|
||||
|
||||
private fun liveLocationReceiver(
|
||||
onLocation: suspend (Location) -> Unit = {},
|
||||
) = object : LiveLocationReceiver {
|
||||
override suspend fun onLocationUpdate(location: Location) = onLocation(location)
|
||||
override suspend fun onUnrecoverableError() = Unit
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.share
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||
@@ -56,6 +57,7 @@ class DefaultShareLocationEntryPointTest {
|
||||
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||
sessionId = room.sessionId,
|
||||
),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+2
-5
@@ -21,6 +21,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
@@ -102,6 +103,7 @@ class ShareLocationPresenterTest {
|
||||
durationFormatter = durationFormatter,
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
liveLocationStore = liveLocationStore,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -118,7 +120,6 @@ class ShareLocationPresenterTest {
|
||||
val state = awaitFirstItem()
|
||||
assertThat(state.customMapStyleUrl.isLoading()).isFalse()
|
||||
assertThat(state.trackUserLocation).isTrue()
|
||||
assertThat(state.hasLocationPermission).isTrue()
|
||||
assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,6 @@ class ShareLocationPresenterTest {
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isTrue()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,6 @@ class ShareLocationPresenterTest {
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied)
|
||||
)
|
||||
@@ -195,7 +194,6 @@ class ShareLocationPresenterTest {
|
||||
shareLocationPresenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale)
|
||||
)
|
||||
@@ -216,7 +214,6 @@ class ShareLocationPresenterTest {
|
||||
shareLocationPresenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.trackUserLocation).isFalse()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.dialogState).isEqualTo(
|
||||
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled)
|
||||
)
|
||||
|
||||
-2
@@ -120,7 +120,6 @@ class ShareLocationViewTest {
|
||||
setShareLocationView(
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
hasLocationPermission = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
navigateUp = EnsureNeverCalled(),
|
||||
@@ -135,7 +134,6 @@ class ShareLocationViewTest {
|
||||
setShareLocationView(
|
||||
aShareLocationState(
|
||||
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||
hasLocationPermission = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
navigateUp = EnsureNeverCalled(),
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||
@@ -51,6 +52,7 @@ class DefaultShowLocationEntryPointTest {
|
||||
joinedRoom = joinedRoom,
|
||||
client = FakeMatrixClient(),
|
||||
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+50
-57
@@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
@@ -41,6 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ShowLocationPresenterTest {
|
||||
@@ -76,26 +78,9 @@ class ShowLocationPresenterTest {
|
||||
joinedRoom = joinedRoom,
|
||||
client = client,
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
)
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.customMapStyleUrl.isLoading()).isTrue()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - non-null customMapStyleUrl`() = runTest {
|
||||
val shareLocationPresenter = createShowLocationPresenter(
|
||||
@@ -112,35 +97,6 @@ class ShowLocationPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state location permission denied once`() = runTest {
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
)
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isFalse()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state with location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state with partial location permission`() = runTest {
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||
@@ -148,7 +104,6 @@ class ShowLocationPresenterTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -176,7 +131,6 @@ class ShowLocationPresenterTest {
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||
|
||||
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
|
||||
@@ -184,7 +138,6 @@ class ShowLocationPresenterTest {
|
||||
|
||||
delay(1)
|
||||
|
||||
assertThat(trackMyLocationState.hasLocationPermission).isTrue()
|
||||
assertThat(trackMyLocationState.isTrackMyLocation).isTrue()
|
||||
|
||||
// Swipe the map to switch mode
|
||||
@@ -192,7 +145,6 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationDisabledState = awaitItem()
|
||||
assertThat(trackLocationDisabledState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationDisabledState.hasLocationPermission).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,14 +167,12 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvent.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +194,6 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Continue the dialog sends permission request to the permissions presenter
|
||||
trackLocationState.eventSink(ShowLocationEvent.RequestPermissions)
|
||||
@@ -271,14 +220,12 @@ class ShowLocationPresenterTest {
|
||||
val trackLocationState = awaitItem()
|
||||
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionDenied)
|
||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvent.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +273,6 @@ class ShowLocationPresenterTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.hasLocationPermission).isTrue()
|
||||
|
||||
// Try to track location when location services are disabled
|
||||
initialState.eventSink(ShowLocationEvent.TrackMyLocation(true))
|
||||
@@ -493,4 +439,51 @@ class ShowLocationPresenterTest {
|
||||
assertThat(state.isSheetDraggable).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static mode never hides user location puck`() = runTest {
|
||||
val presenter = createShowLocationPresenter()
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live mode does not hide user location puck when not sharing`() = runTest {
|
||||
val presenter = createShowLocationPresenter(
|
||||
mode = ShowLocationMode.Live(senderId = A_USER_ID),
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live mode user location state uses own share position when sharing`() = runTest {
|
||||
val ownLiveLocationShare = aLiveLocationShare()
|
||||
val fakeRoom = FakeJoinedRoom(
|
||||
liveLocationSharesFlow = MutableStateFlow(listOf(ownLiveLocationShare))
|
||||
)
|
||||
val manager = FakeActiveLiveLocationShareManager(
|
||||
startShareLambda = { _, _ -> Result.success(Unit) }
|
||||
)
|
||||
manager.startShare(fakeRoom.roomId, 1.hours)
|
||||
|
||||
val presenter = createShowLocationPresenter(
|
||||
mode = ShowLocationMode.Live(senderId = A_USER_ID),
|
||||
joinedRoom = fakeRoom,
|
||||
liveLocationShareManager = manager,
|
||||
)
|
||||
val ownLocation = ownLiveLocationShare.lastLocation?.geoUri?.let(Location::fromGeoUri)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.hideUserLocationPuck).isTrue()
|
||||
val location = requireNotNull(state.userLocationState.location)
|
||||
assertThat(location.position.value.latitude).isEqualTo(ownLocation?.lat)
|
||||
assertThat(location.position.value.longitude).isEqualTo(ownLocation?.lon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Zadajte prosím ten istý PIN dvakrát"</string>
|
||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN kódy sa nezhodujú"</string>
|
||||
<string name="screen_app_lock_signout_alert_message">"Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód."</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"Prebieha odhlasovanie"</string>
|
||||
<string name="screen_app_lock_signout_alert_title">"Toto zariadenie sa odstraňuje"</string>
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="one">"Máte %1$d pokus na odomknutie"</item>
|
||||
<item quantity="few">"Máte %1$d pokusy na odomknutie"</item>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<string name="screen_change_server_subtitle">"서버의 주소는 무엇인가요?"</string>
|
||||
<string name="screen_change_server_title">"서버 선택"</string>
|
||||
<string name="screen_create_account_title">"계정 만들기"</string>
|
||||
<string name="screen_login_error_deactivated_account">"계정이 비활성화되었습니다."</string>
|
||||
<string name="screen_login_error_deactivated_account">"삭제된 계정입니다."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"잘못된 아이디/비밀번호"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"이 사용자 ID는 유효하지 않습니다. 예상 형식: ‘@user:homeserver.org’"</string>
|
||||
<string name="screen_login_error_refresh_tokens">"이 서버는 새로 고침 토큰을 사용하도록 구성되어 있습니다. 비밀번호 기반 로그인을 사용하는 경우 이 기능은 지원되지 않습니다."</string>
|
||||
@@ -45,6 +45,7 @@
|
||||
<string name="screen_missing_key_backup_step_5">"%1$s(으)로 돌아가기"</string>
|
||||
<string name="screen_missing_key_backup_title">"%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요."</string>
|
||||
<string name="screen_onboarding_app_version">"버전 %1$s"</string>
|
||||
<string name="screen_onboarding_checking_account">"계정 확인 중"</string>
|
||||
<string name="screen_onboarding_sign_in_manually">"수동으로 로그인"</string>
|
||||
<string name="screen_onboarding_sign_in_to">"%1$s 에 로그인합니다"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"QR 코드로 로그인"</string>
|
||||
|
||||
@@ -97,7 +97,7 @@ Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu."</st
|
||||
<string name="screen_qr_code_login_start_over_button">"Zacznij od nowa"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Wystąpił nieoczekiwany błąd. Spróbuj ponownie."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Oczekiwanie na drugie urządzenie"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Twój dostawca konta może poprosić o podany kod, aby zweryfikować logowanie."</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Twój dostawca konta może poprosić o podany kod w celu weryfikacji logowania."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Twój kod weryfikacyjny"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="screen_signout_key_backup_ongoing_title">"Vaše kľúče sa ešte stále zálohujú"</string>
|
||||
<string name="screen_signout_preference_item">"Odstrániť toto zariadenie"</string>
|
||||
<string name="screen_signout_recovery_disabled_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."</string>
|
||||
<string name="screen_signout_recovery_disabled_title">"Obnovenie nie je nastavené"</string>
|
||||
<string name="screen_signout_recovery_disabled_title">"Čoskoro stratíte prístup k svojim zašifrovaným konverzáciám"</string>
|
||||
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam."</string>
|
||||
<string name="screen_signout_save_recovery_key_title">"Uložili ste si kľúč na obnovenie?"</string>
|
||||
</resources>
|
||||
|
||||
+49
-15
@@ -11,6 +11,9 @@ package io.element.android.features.messages.impl
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
@@ -29,6 +32,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.features.location.api.LocationService
|
||||
import io.element.android.features.location.api.RenderingMapsNotSupportedDialog
|
||||
import io.element.android.features.location.api.ShareLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
@@ -53,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.duration
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.features.poll.api.create.CreatePollMode
|
||||
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
@@ -121,6 +126,7 @@ class MessagesFlowNode(
|
||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val hasVulkanSupport: DeviceHasVulkanSupport,
|
||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||
@@ -188,6 +194,8 @@ class MessagesFlowNode(
|
||||
|
||||
private val callback: MessagesEntryPoint.Callback = callback()
|
||||
|
||||
private var displayVulkanNotSupportedError by mutableStateOf(false)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
@@ -267,7 +275,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToSendLocation() {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Live))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToCreatePoll() {
|
||||
@@ -279,7 +291,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToCurrentLiveLocation() {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||
@@ -506,7 +522,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToSendLocation() {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToCreatePoll() {
|
||||
@@ -518,7 +538,11 @@ class MessagesFlowNode(
|
||||
}
|
||||
|
||||
override fun navigateToCurrentLiveLocation() {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
if (hasVulkanSupport()) {
|
||||
backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId)))
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) {
|
||||
@@ -607,18 +631,23 @@ class MessagesFlowNode(
|
||||
)
|
||||
}
|
||||
is TimelineItemLocationContent -> {
|
||||
val mode = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||
location = event.content.mode.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
if (hasVulkanSupport()) {
|
||||
val mode = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||
location = event.content.mode.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
}
|
||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||
} else {
|
||||
displayVulkanNotSupportedError = true
|
||||
null
|
||||
}
|
||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -691,6 +720,11 @@ class MessagesFlowNode(
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
mentionSpanTheme.updateStyles()
|
||||
|
||||
if (displayVulkanNotSupportedError) {
|
||||
RenderingMapsNotSupportedDialog { displayVulkanNotSupportedError = false }
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMentionSpanUpdater provides mentionSpanUpdater
|
||||
) {
|
||||
|
||||
+2
@@ -17,6 +17,8 @@ sealed interface AttachmentsPreviewEvent {
|
||||
data object OpenImageEditor : AttachmentsPreviewEvent
|
||||
data object CloseImageEditor : AttachmentsPreviewEvent
|
||||
data object RotateImageToTheLeft : AttachmentsPreviewEvent
|
||||
data object FlipImageHorizontally : AttachmentsPreviewEvent
|
||||
data object FlipImageVertically : AttachmentsPreviewEvent
|
||||
data object ApplyImageEdits : AttachmentsPreviewEvent
|
||||
data object ResetImageEdits : AttachmentsPreviewEvent
|
||||
data class UpdateImageCropRect(val cropRect: NormalizedCropRect) : AttachmentsPreviewEvent
|
||||
|
||||
+12
@@ -295,6 +295,18 @@ class AttachmentsPreviewPresenter(
|
||||
edits = pendingState.edits.rotateAntiClockwise()
|
||||
)
|
||||
}
|
||||
AttachmentsPreviewEvent.FlipImageHorizontally -> {
|
||||
val pendingState = imageEditorState ?: return
|
||||
imageEditorState = pendingState.copy(
|
||||
edits = pendingState.edits.flipHorizontally()
|
||||
)
|
||||
}
|
||||
AttachmentsPreviewEvent.FlipImageVertically -> {
|
||||
val pendingState = imageEditorState ?: return
|
||||
imageEditorState = pendingState.copy(
|
||||
edits = pendingState.edits.flipVertically()
|
||||
)
|
||||
}
|
||||
AttachmentsPreviewEvent.ResetImageEdits -> {
|
||||
imageEditorState = imageEditorState?.copy(
|
||||
edits = AttachmentImageEdits()
|
||||
|
||||
+13
-5
@@ -37,6 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
@@ -57,11 +58,12 @@ import io.element.android.libraries.designsystem.modifiers.niceClickable
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Switch
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
@@ -136,6 +138,8 @@ fun AttachmentsPreviewView(
|
||||
state.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect))
|
||||
},
|
||||
onRotateClick = { state.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) },
|
||||
onFlipHorizontallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally) },
|
||||
onFlipVerticallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageVertically) },
|
||||
onCancelClick = ::postCloseImageEditor,
|
||||
onResetClick = ::postResetImageEditor,
|
||||
onDoneClick = ::postApplyImageEdits,
|
||||
@@ -161,10 +165,14 @@ fun AttachmentsPreviewView(
|
||||
},
|
||||
actions = {
|
||||
if (state.canEditImage && canShowEditAction) {
|
||||
TextButton(
|
||||
stringResource(CommonStrings.action_edit),
|
||||
onClick = ::postOpenImageEditor
|
||||
)
|
||||
IconButton(
|
||||
onClick = ::postOpenImageEditor,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Crop(),
|
||||
contentDescription = stringResource(CommonStrings.action_edit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
+24
-13
@@ -87,30 +87,30 @@ class DefaultAttachmentImageEditor(
|
||||
decodedBitmap.recycle()
|
||||
}
|
||||
|
||||
val rotatedBitmap = normalizedBitmap.rotateQuarterTurns(edits.rotationQuarterTurns)
|
||||
if (rotatedBitmap !== normalizedBitmap) {
|
||||
val transformedBitmap = normalizedBitmap.applyEdits(edits)
|
||||
if (transformedBitmap !== normalizedBitmap) {
|
||||
normalizedBitmap.recycle()
|
||||
}
|
||||
|
||||
val cropRect = edits.cropRect.toPixelRect(
|
||||
imageWidth = rotatedBitmap.width,
|
||||
imageHeight = rotatedBitmap.height,
|
||||
imageWidth = transformedBitmap.width,
|
||||
imageHeight = transformedBitmap.height,
|
||||
)
|
||||
val isCropUnchanged = cropRect.left == 0 && cropRect.top == 0 &&
|
||||
cropRect.width() == rotatedBitmap.width && cropRect.height() == rotatedBitmap.height
|
||||
cropRect.width() == transformedBitmap.width && cropRect.height() == transformedBitmap.height
|
||||
val croppedBitmap = if (isCropUnchanged) {
|
||||
rotatedBitmap
|
||||
transformedBitmap
|
||||
} else {
|
||||
Bitmap.createBitmap(
|
||||
rotatedBitmap,
|
||||
transformedBitmap,
|
||||
cropRect.left,
|
||||
cropRect.top,
|
||||
cropRect.width(),
|
||||
cropRect.height(),
|
||||
)
|
||||
}
|
||||
if (croppedBitmap !== rotatedBitmap) {
|
||||
rotatedBitmap.recycle()
|
||||
if (croppedBitmap !== transformedBitmap) {
|
||||
transformedBitmap.recycle()
|
||||
}
|
||||
|
||||
val editedMediaDir = File(context.cacheDir, EDITED_MEDIA_DIR_NAME).apply { mkdirs() }
|
||||
@@ -141,11 +141,22 @@ internal fun exportedMimeTypeFor(sourceMimeType: String?): String {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.rotateQuarterTurns(quarterTurns: Int): Bitmap {
|
||||
val normalizedTurns = (quarterTurns % 4 + 4) % 4
|
||||
if (normalizedTurns == 0) return this
|
||||
private fun Bitmap.applyEdits(edits: AttachmentImageEdits): Bitmap {
|
||||
val normalizedTurns = (edits.rotationQuarterTurns % 4 + 4) % 4
|
||||
if (normalizedTurns == 0 && !edits.isFlippedHorizontally && !edits.isFlippedVertically) {
|
||||
return this
|
||||
}
|
||||
val centerX = width / 2f
|
||||
val centerY = height / 2f
|
||||
val matrix = Matrix().apply {
|
||||
postRotate(normalizedTurns * 90f)
|
||||
val scaleX = if (edits.isFlippedHorizontally) -1f else 1f
|
||||
val scaleY = if (edits.isFlippedVertically) -1f else 1f
|
||||
if (scaleX < 0f || scaleY < 0f) {
|
||||
postScale(scaleX, scaleY, centerX, centerY)
|
||||
}
|
||||
if (normalizedTurns != 0) {
|
||||
postRotate(normalizedTurns * 90f, centerX, centerY)
|
||||
}
|
||||
}
|
||||
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
||||
}
|
||||
|
||||
+40
-7
@@ -26,6 +26,8 @@ data class AttachmentImageEditorState(
|
||||
data class AttachmentImageEdits(
|
||||
val cropRect: NormalizedCropRect = NormalizedCropRect.default(),
|
||||
val rotationQuarterTurns: Int = 0,
|
||||
val isFlippedHorizontally: Boolean = false,
|
||||
val isFlippedVertically: Boolean = false,
|
||||
) {
|
||||
val normalizedRotationQuarterTurns: Int
|
||||
get() = rotationQuarterTurns % 4
|
||||
@@ -34,18 +36,32 @@ data class AttachmentImageEdits(
|
||||
get() = normalizedRotationQuarterTurns * 90
|
||||
|
||||
val hasChanges: Boolean
|
||||
get() = cropRect != NormalizedCropRect.default() || normalizedRotationQuarterTurns != 0
|
||||
get() = cropRect != NormalizedCropRect.default() ||
|
||||
normalizedRotationQuarterTurns != 0 ||
|
||||
isFlippedHorizontally ||
|
||||
isFlippedVertically
|
||||
|
||||
fun rotateAntiClockwise(): AttachmentImageEdits {
|
||||
return copy(
|
||||
rotationQuarterTurns = (normalizedRotationQuarterTurns + 3) % 4,
|
||||
// Also update the crop rect to keep the same selected area
|
||||
cropRect = NormalizedCropRect(
|
||||
left = cropRect.top,
|
||||
top = 1f - cropRect.right,
|
||||
right = cropRect.bottom,
|
||||
bottom = 1f - cropRect.left,
|
||||
)
|
||||
cropRect = cropRect.rotateAntiClockwise()
|
||||
)
|
||||
}
|
||||
|
||||
fun flipHorizontally(): AttachmentImageEdits {
|
||||
return copy(
|
||||
isFlippedHorizontally = !isFlippedHorizontally,
|
||||
// Also update the crop rect to keep the same selected area
|
||||
cropRect = cropRect.flipHorizontally(),
|
||||
)
|
||||
}
|
||||
|
||||
fun flipVertically(): AttachmentImageEdits {
|
||||
return copy(
|
||||
isFlippedVertically = !isFlippedVertically,
|
||||
// Also update the crop rect to keep the same selected area
|
||||
cropRect = cropRect.flipVertically(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +151,23 @@ data class NormalizedCropRect(
|
||||
)
|
||||
}
|
||||
|
||||
fun rotateAntiClockwise() = copy(
|
||||
left = top,
|
||||
top = 1f - right,
|
||||
right = bottom,
|
||||
bottom = 1f - left,
|
||||
)
|
||||
|
||||
fun flipHorizontally() = copy(
|
||||
left = 1f - right,
|
||||
right = 1f - left,
|
||||
)
|
||||
|
||||
fun flipVertically() = copy(
|
||||
top = 1f - bottom,
|
||||
bottom = 1f - top,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun default() = NormalizedCropRect(
|
||||
left = DEFAULT_CROP_MARGIN,
|
||||
|
||||
+10
@@ -75,6 +75,16 @@ open class AttachmentImageEditorStateProvider : PreviewParameterProvider<Attachm
|
||||
),
|
||||
previewDebug = true,
|
||||
),
|
||||
anAttachmentImageEditorState(
|
||||
edits = AttachmentImageEdits(
|
||||
cropRect = caterpillarCrop,
|
||||
).flipHorizontally(),
|
||||
),
|
||||
anAttachmentImageEditorState(
|
||||
edits = AttachmentImageEdits(
|
||||
cropRect = caterpillarCrop,
|
||||
).flipVertically(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+57
-15
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.attachments.preview.imageedito
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -90,6 +88,8 @@ fun AttachmentImageEditorView(
|
||||
state: AttachmentImageEditorState,
|
||||
onCropRectChange: (NormalizedCropRect) -> Unit,
|
||||
onRotateClick: () -> Unit,
|
||||
onFlipHorizontallyClick: () -> Unit,
|
||||
onFlipVerticallyClick: () -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
@@ -101,8 +101,18 @@ fun AttachmentImageEditorView(
|
||||
state.edits.rotationDegrees,
|
||||
state.edits.rotationDegrees,
|
||||
)
|
||||
val rotateButtonBackground = ElementTheme.colors.bgCanvasDefault
|
||||
|
||||
val flipHorizontalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally)
|
||||
val flipHorizontalState = if (state.edits.isFlippedHorizontally) {
|
||||
stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_flipped)
|
||||
} else {
|
||||
stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_original)
|
||||
}
|
||||
val flipVerticalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_vertically)
|
||||
val flipVerticalState = if (state.edits.isFlippedVertically) {
|
||||
stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_flipped)
|
||||
} else {
|
||||
stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_original)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
@@ -158,37 +168,57 @@ fun AttachmentImageEditorView(
|
||||
onClick = onResetClick,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center,
|
||||
Row(
|
||||
modifier = Modifier.weight(2f),
|
||||
// Center the content horizontally
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onFlipHorizontallyClick,
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = flipHorizontalLabel
|
||||
stateDescription = flipHorizontalState
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.FlipHorizontal(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onRotateClick,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = rotateButtonBackground,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, CircleShape)
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = rotateContentDescription
|
||||
stateDescription = rotationStateDescription
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(22.dp),
|
||||
imageVector = CompoundIcons.RotateLeft(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onFlipVerticallyClick,
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = flipVerticalLabel
|
||||
stateDescription = flipVerticalState
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.FlipVertical(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_done),
|
||||
text = stringResource(CommonStrings.action_save),
|
||||
onClick = onDoneClick,
|
||||
)
|
||||
}
|
||||
@@ -204,6 +234,8 @@ private fun BoxScope.CropEditorCanvas(
|
||||
) {
|
||||
var imageSize by remember(state.localMedia.uri) { mutableStateOf(IntSize.Zero) }
|
||||
val rotationQuarterTurns = state.edits.normalizedRotationQuarterTurns
|
||||
val flipScaleX = if (state.edits.isFlippedHorizontally) -1f else 1f
|
||||
val flipScaleY = if (state.edits.isFlippedVertically) -1f else 1f
|
||||
|
||||
var imageRect by remember { mutableStateOf(Rect.Zero) }
|
||||
|
||||
@@ -257,6 +289,10 @@ private fun BoxScope.CropEditorCanvas(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
||||
.graphicsLayer {
|
||||
scaleX = flipScaleX
|
||||
scaleY = flipScaleY
|
||||
}
|
||||
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
@@ -266,6 +302,10 @@ private fun BoxScope.CropEditorCanvas(
|
||||
contentDescription = stringResource(CommonStrings.common_image),
|
||||
modifier = Modifier
|
||||
.requiredSize(imageLayoutWidthDp, imageLayoutHeightDp)
|
||||
.graphicsLayer {
|
||||
scaleX = flipScaleX
|
||||
scaleY = flipScaleY
|
||||
}
|
||||
.graphicsLayer { rotationZ = rotationQuarterTurns * 90f },
|
||||
contentScale = ContentScale.Fit,
|
||||
onState = { painterState ->
|
||||
@@ -642,6 +682,8 @@ internal fun AttachmentImageEditorViewPreview(
|
||||
state = state,
|
||||
onCropRectChange = {},
|
||||
onRotateClick = {},
|
||||
onFlipHorizontallyClick = {},
|
||||
onFlipVerticallyClick = {},
|
||||
onResetClick = {},
|
||||
onCancelClick = {},
|
||||
onDoneClick = {},
|
||||
|
||||
+2
-1
@@ -208,7 +208,8 @@ class PinnedMessagesListPresenter(
|
||||
combine(timelineItemsFlow, room.membersStateFlow) { items, membersState ->
|
||||
timelineItemsFactory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = membersState.roomMembers().orEmpty()
|
||||
roomMembers = membersState.roomMembers().orEmpty(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
}.launchIn(this)
|
||||
|
||||
|
||||
-1
@@ -217,7 +217,6 @@ private fun PinnedMessagesListLoaded(
|
||||
timelineItem = timelineItem,
|
||||
timelineMode = Timeline.Mode.PinnedEvents,
|
||||
timelineRoomInfo = state.timelineRoomInfo,
|
||||
renderReadReceipts = false,
|
||||
timelineProtectionState = state.timelineProtectionState,
|
||||
isLastOutgoingMessage = false,
|
||||
focusedEventId = null,
|
||||
|
||||
+7
-10
@@ -146,9 +146,6 @@ class TimelinePresenter(
|
||||
val suppressJumpToUnread = remember { mutableStateOf(false) }
|
||||
|
||||
val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present()
|
||||
val renderReadReceipts by remember {
|
||||
sessionPreferencesStore.isRenderReadReceiptsEnabled()
|
||||
}.collectAsState(initial = true)
|
||||
val isLive by remember {
|
||||
timelineController.isLive()
|
||||
}.collectAsState(initial = true)
|
||||
@@ -156,9 +153,6 @@ class TimelinePresenter(
|
||||
val displayThreadSummaries by produceState(false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
||||
}
|
||||
val displayFloatingDateBadge by produceState(false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.FloatingDateBadge)
|
||||
}
|
||||
val displayJumpToUnread by produceState(false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.JumpToUnread)
|
||||
}
|
||||
@@ -274,13 +268,18 @@ class TimelinePresenter(
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
|
||||
combine(
|
||||
timelineController.timelineItems(),
|
||||
room.membersStateFlow,
|
||||
sessionPreferencesStore.isRenderReadReceiptsEnabled(),
|
||||
) { items, membersState, renderReadReceipts ->
|
||||
val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems)
|
||||
val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items")
|
||||
transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString())
|
||||
timelineItemsFactory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = membersState.roomMembers().orEmpty()
|
||||
roomMembers = membersState.roomMembers().orEmpty(),
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
)
|
||||
transaction?.finish()
|
||||
items
|
||||
@@ -379,14 +378,12 @@ class TimelinePresenter(
|
||||
timelineItems = timelineItems,
|
||||
timelineMode = timelineMode,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
newEventState = newEventState.value,
|
||||
isLive = isLive,
|
||||
focusRequestState = focusRequestState.value,
|
||||
messageShieldDialogData = messageShieldDialogData.value,
|
||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||
displayJumpToUnread = displayJumpToUnread,
|
||||
jumpToUnread = jumpToUnread.value,
|
||||
eventSink = ::handleEvent,
|
||||
|
||||
-2
@@ -26,7 +26,6 @@ data class TimelineState(
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val timelineRoomInfo: TimelineRoomInfo,
|
||||
val timelineMode: Timeline.Mode,
|
||||
val renderReadReceipts: Boolean,
|
||||
val newEventState: NewEventState,
|
||||
val isLive: Boolean,
|
||||
val focusRequestState: FocusRequestState,
|
||||
@@ -34,7 +33,6 @@ data class TimelineState(
|
||||
val messageShieldDialogData: MessageShieldData?,
|
||||
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
||||
val displayThreadSummaries: Boolean,
|
||||
val displayFloatingDateBadge: Boolean,
|
||||
val displayJumpToUnread: Boolean,
|
||||
val jumpToUnread: JumpToUnreadState,
|
||||
val eventSink: (TimelineEvent) -> Unit,
|
||||
|
||||
-4
@@ -51,14 +51,12 @@ import kotlin.random.Random
|
||||
fun aTimelineState(
|
||||
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
|
||||
timelineMode: Timeline.Mode = Timeline.Mode.Live,
|
||||
renderReadReceipts: Boolean = false,
|
||||
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
||||
focusedEventIndex: Int = -1,
|
||||
isLive: Boolean = true,
|
||||
messageShield: MessageShield? = null,
|
||||
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
|
||||
displayThreadSummaries: Boolean = false,
|
||||
displayFloatingDateBadge: Boolean = false,
|
||||
displayJumpToUnread: Boolean = false,
|
||||
jumpToUnread: JumpToUnreadState = JumpToUnreadState.Hidden,
|
||||
newEventState: NewEventState = NewEventState.None,
|
||||
@@ -74,14 +72,12 @@ fun aTimelineState(
|
||||
timelineItems = timelineItems,
|
||||
timelineMode = timelineMode,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
newEventState = newEventState,
|
||||
isLive = isLive,
|
||||
focusRequestState = focusRequestState,
|
||||
messageShieldDialogData = messageShield?.let { MessageShieldData(it) },
|
||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||
displayJumpToUnread = displayJumpToUnread,
|
||||
jumpToUnread = jumpToUnread,
|
||||
eventSink = eventSink,
|
||||
|
||||
+1
-2
@@ -207,7 +207,6 @@ fun TimelineView(
|
||||
timelineMode = state.timelineMode,
|
||||
timelineRoomInfo = state.timelineRoomInfo,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
renderReadReceipts = state.renderReadReceipts,
|
||||
isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()),
|
||||
focusedEventId = state.focusedEventId,
|
||||
displayThreadSummaries = state.displayThreadSummaries,
|
||||
@@ -254,7 +253,7 @@ fun TimelineView(
|
||||
onFocusOnEvent = ::onFocusOnEvent,
|
||||
)
|
||||
|
||||
if (state.displayFloatingDateBadge && useReverseLayout) {
|
||||
if (useReverseLayout) {
|
||||
FloatingDateBadgeOverlay(
|
||||
lazyListState = lazyListState,
|
||||
timelineItems = state.timelineItems,
|
||||
|
||||
-2
@@ -22,7 +22,6 @@ internal fun ATimelineItemEventRow(
|
||||
event: TimelineItem.Event,
|
||||
timelineMode: Timeline.Mode = Timeline.Mode.Live,
|
||||
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
||||
renderReadReceipts: Boolean = false,
|
||||
isLastOutgoingMessage: Boolean = false,
|
||||
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
|
||||
displayThreadSummaries: Boolean = false,
|
||||
@@ -30,7 +29,6 @@ internal fun ATimelineItemEventRow(
|
||||
event = event,
|
||||
timelineMode = timelineMode,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
|
||||
+6
-9
@@ -52,7 +52,6 @@ internal fun TimelineItemCallNotifyView(
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
event: TimelineItem.Event,
|
||||
content: TimelineItemRtcNotificationContent,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
onReadReceiptsClick: (TimelineItem.Event) -> Unit,
|
||||
@@ -106,7 +105,6 @@ internal fun TimelineItemCallNotifyView(
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
receipts = event.readReceiptState.receipts,
|
||||
),
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
onReadReceiptsClick = { onReadReceiptsClick(event) },
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
@@ -146,8 +144,10 @@ private fun getIcon(
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
||||
val readReceiptState = aTimelineItemReadReceipts(
|
||||
receipts = List(3) { aReadReceiptData(it) },
|
||||
val readReceiptState = mutableListOf(
|
||||
aTimelineItemReadReceipts(
|
||||
receipts = List(3) { aReadReceiptData(it) },
|
||||
)
|
||||
)
|
||||
Column(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||
listOf(false, true).forEach { isDm ->
|
||||
@@ -162,13 +162,10 @@ internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
||||
timelineRoomInfo = aTimelineRoomInfo(isDm = isDm),
|
||||
event = aTimelineItemEvent(
|
||||
content = content,
|
||||
readReceiptState = readReceiptState,
|
||||
// Only display read receipts for the first item
|
||||
readReceiptState = readReceiptState.removeFirstOrNull() ?: aTimelineItemReadReceipts(),
|
||||
),
|
||||
content = content,
|
||||
// Render read receipts for the first item only
|
||||
renderReadReceipts = !isDm &&
|
||||
callIntent == CallIntent.AUDIO &&
|
||||
state == RtcNotificationState.Started,
|
||||
isLastOutgoingMessage = false,
|
||||
onLongClick = {},
|
||||
onReadReceiptsClick = {},
|
||||
|
||||
-2
@@ -142,7 +142,6 @@ fun TimelineItemEventRow(
|
||||
timelineMode: Timeline.Mode,
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
displayThreadSummaries: Boolean,
|
||||
onEventClick: () -> Unit,
|
||||
@@ -296,7 +295,6 @@ fun TimelineItemEventRow(
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
receipts = event.readReceiptState.receipts,
|
||||
),
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
onReadReceiptsClick = { onReadReceiptClick(event) },
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
-3
@@ -37,7 +37,6 @@ internal fun TimelineItemEventRowWithRRPreview(
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
readReceiptState = TimelineItemReadReceipts(state.receipts),
|
||||
),
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
)
|
||||
// A message from current user
|
||||
@@ -49,7 +48,6 @@ internal fun TimelineItemEventRowWithRRPreview(
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
readReceiptState = TimelineItemReadReceipts(state.receipts),
|
||||
),
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
)
|
||||
// Another message from current user
|
||||
@@ -61,7 +59,6 @@ internal fun TimelineItemEventRowWithRRPreview(
|
||||
timelineItemReactions = aTimelineItemReactions(count = 0),
|
||||
readReceiptState = TimelineItemReadReceipts(state.receipts),
|
||||
),
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = true,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-8
@@ -43,7 +43,6 @@ fun TimelineItemGroupedEventsRow(
|
||||
timelineMode: Timeline.Mode,
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
focusedEventId: EventId?,
|
||||
displayThreadSummaries: Boolean,
|
||||
@@ -89,7 +88,6 @@ fun TimelineItemGroupedEventsRow(
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
focusedEventId = focusedEventId,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
onClick = onClick,
|
||||
@@ -117,7 +115,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
focusedEventId: EventId?,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
displayThreadSummaries: Boolean,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
@@ -173,7 +170,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
timelineItem = subGroupEvent,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
focusedEventId = focusedEventId,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
@@ -193,14 +189,13 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (renderReadReceipts) {
|
||||
} else if (timelineItem.aggregatedReadReceipts.isNotEmpty()) {
|
||||
TimelineItemReadReceiptView(
|
||||
state = ReadReceiptViewState(
|
||||
sendState = null,
|
||||
isLastOutgoingMessage = false,
|
||||
receipts = timelineItem.aggregatedReadReceipts,
|
||||
),
|
||||
renderReadReceipts = true,
|
||||
onReadReceiptsClick = onExpandGroupClick
|
||||
)
|
||||
}
|
||||
@@ -219,7 +214,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||
timelineRoomInfo = aTimelineRoomInfo(),
|
||||
timelineProtectionState = aTimelineProtectionState(),
|
||||
focusedEventId = events.events.first().eventId,
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
displayThreadSummaries = false,
|
||||
onClick = {},
|
||||
@@ -247,7 +241,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||
timelineRoomInfo = aTimelineRoomInfo(),
|
||||
timelineProtectionState = aTimelineProtectionState(),
|
||||
focusedEventId = null,
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
displayThreadSummaries = false,
|
||||
onClick = {},
|
||||
|
||||
-5
@@ -56,7 +56,6 @@ internal fun TimelineItemRow(
|
||||
timelineItem: TimelineItem,
|
||||
timelineMode: Timeline.Mode,
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
focusedEventId: EventId?,
|
||||
@@ -114,7 +113,6 @@ internal fun TimelineItemRow(
|
||||
is TimelineItemStateContent, is TimelineItemLegacyCallInviteContent -> {
|
||||
TimelineItemStateEventRow(
|
||||
event = timelineItem,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
onClick = { onContentClick(timelineItem) },
|
||||
onReadReceiptsClick = onReadReceiptClick,
|
||||
@@ -127,7 +125,6 @@ internal fun TimelineItemRow(
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
event = timelineItem,
|
||||
content = timelineItem.content,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
onLongClick = onLongClick,
|
||||
onReadReceiptsClick = onReadReceiptClick,
|
||||
@@ -167,7 +164,6 @@ internal fun TimelineItemRow(
|
||||
event = timelineItem,
|
||||
timelineMode = timelineMode,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
@@ -196,7 +192,6 @@ internal fun TimelineItemRow(
|
||||
timelineMode = timelineMode,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
focusedEventId = focusedEventId,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
|
||||
-3
@@ -39,7 +39,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
event: TimelineItem.Event,
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
@@ -86,7 +85,6 @@ fun TimelineItemStateEventRow(
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
receipts = event.readReceiptState.receipts,
|
||||
),
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
onReadReceiptsClick = { onReadReceiptsClick(event) },
|
||||
)
|
||||
}
|
||||
@@ -104,7 +102,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
|
||||
receipts = persistentListOf(aReadReceiptData(0)),
|
||||
)
|
||||
),
|
||||
renderReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
|
||||
+13
-17
@@ -53,27 +53,24 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Composable
|
||||
fun TimelineItemReadReceiptView(
|
||||
state: ReadReceiptViewState,
|
||||
renderReadReceipts: Boolean,
|
||||
onReadReceiptsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.receipts.isNotEmpty()) {
|
||||
if (renderReadReceipts) {
|
||||
ReadReceiptsRow(
|
||||
modifier = modifier.clearAndSetSemantics {
|
||||
hideFromAccessibility()
|
||||
}
|
||||
) {
|
||||
ReadReceiptsAvatars(
|
||||
receipts = state.receipts,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable {
|
||||
onReadReceiptsClick()
|
||||
}
|
||||
.padding(2.dp)
|
||||
)
|
||||
ReadReceiptsRow(
|
||||
modifier = modifier.clearAndSetSemantics {
|
||||
hideFromAccessibility()
|
||||
}
|
||||
) {
|
||||
ReadReceiptsAvatars(
|
||||
receipts = state.receipts,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable {
|
||||
onReadReceiptsClick()
|
||||
}
|
||||
.padding(2.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (state.sendState) {
|
||||
@@ -209,7 +206,6 @@ internal fun TimelineItemReadReceiptViewPreview(
|
||||
) = ElementPreview {
|
||||
TimelineItemReadReceiptView(
|
||||
state = state,
|
||||
renderReadReceipts = true,
|
||||
onReadReceiptsClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
+8
-4
@@ -64,22 +64,24 @@ class TimelineItemsFactory(
|
||||
suspend fun replaceWith(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
) = withContext(dispatchers.computation) {
|
||||
lock.withLock {
|
||||
diffCacheUpdater.updateWith(timelineItems)
|
||||
buildAndEmitTimelineItemStates(timelineItems, roomMembers)
|
||||
buildAndEmitTimelineItemStates(timelineItems, roomMembers, renderReadReceipts)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildAndEmitTimelineItemStates(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
) {
|
||||
val newTimelineItemStates = ArrayList<TimelineItem>()
|
||||
for (index in diffCache.indices().reversed()) {
|
||||
val cacheItem = diffCache.get(index)
|
||||
if (cacheItem == null) {
|
||||
buildAndCacheItem(timelineItems, index, roomMembers)?.also { timelineItemState ->
|
||||
buildAndCacheItem(timelineItems, index, roomMembers, renderReadReceipts)?.also { timelineItemState ->
|
||||
newTimelineItemStates.add(timelineItemState)
|
||||
}
|
||||
} else {
|
||||
@@ -87,7 +89,8 @@ class TimelineItemsFactory(
|
||||
eventItemFactory.update(
|
||||
timelineItem = cacheItem,
|
||||
receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event,
|
||||
roomMembers = roomMembers
|
||||
roomMembers = roomMembers,
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
)
|
||||
} else {
|
||||
cacheItem
|
||||
@@ -103,10 +106,11 @@ class TimelineItemsFactory(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
index: Int,
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
): TimelineItem? {
|
||||
val timelineItem =
|
||||
when (val currentTimelineItem = timelineItems[index]) {
|
||||
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers)
|
||||
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers, renderReadReceipts)
|
||||
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
|
||||
MatrixTimelineItem.Other -> null
|
||||
}
|
||||
|
||||
+6
-3
@@ -57,6 +57,7 @@ class TimelineItemEventFactory(
|
||||
index: Int,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
): TimelineItem.Event {
|
||||
val currentSender = currentTimelineItem.event.sender
|
||||
val groupPosition =
|
||||
@@ -116,7 +117,7 @@ class TimelineItemEventFactory(
|
||||
sentDate = sentDate,
|
||||
groupPosition = groupPosition,
|
||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
||||
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts),
|
||||
localSendState = currentTimelineItem.event.localSendState,
|
||||
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
||||
threadInfo = mappedThreadInfo,
|
||||
@@ -133,9 +134,10 @@ class TimelineItemEventFactory(
|
||||
timelineItem: TimelineItem.Event,
|
||||
receivedMatrixTimelineItem: MatrixTimelineItem.Event,
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
): TimelineItem.Event {
|
||||
return timelineItem.copy(
|
||||
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers)
|
||||
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -180,8 +182,9 @@ class TimelineItemEventFactory(
|
||||
|
||||
private fun MatrixTimelineItem.Event.computeReadReceiptState(
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
): TimelineItemReadReceipts {
|
||||
if (!config.computeReadReceipts) {
|
||||
if (!config.computeReadReceipts || !renderReadReceipts) {
|
||||
return TimelineItemReadReceipts(receipts = persistentListOf())
|
||||
}
|
||||
return TimelineItemReadReceipts(
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
<string name="emoji_picker_category_places">"Reisimine ja kohad"</string>
|
||||
<string name="emoji_picker_category_recent">"Hiljutised emojid"</string>
|
||||
<string name="emoji_picker_category_symbols">"Sümbolid"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Pööra pilti ümber rõhttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Pilt on pööratud ümber rõhttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Pildi algne variant"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Pööra pilti ümber püsttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Pilt on pööratud ümber püsttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Pildi algne variant"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Pööra pilti vasakule"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d kraad"</item>
|
||||
|
||||
@@ -16,10 +16,18 @@
|
||||
<string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string>
|
||||
<string name="emoji_picker_category_recent">"Viimeaikaiset emojit"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbolit"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Käännä kuva vaakasuunnassa"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Käännetty vaakasuunnassa"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Alkuperäinen"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Käännä kuva pystysuunnassa"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Käännetty pystysuunnassa"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Alkuperäinen"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Kierrä kuvaa vasemmalle"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d aste"</item>
|
||||
<item quantity="other">"%1$d astetta"</item>
|
||||
</plurals>
|
||||
<string name="screen_image_edition_title">"Muokkaa kuvaa"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Napauta muuttaaksesi videon lähetyslaatua"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Tiedostoa ei voitu lähettää."</string>
|
||||
@@ -30,6 +38,7 @@
|
||||
<string name="screen_media_upload_preview_item_count">"Kohde %1$d / %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimoi kuvanlaatu"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Käsitellään…"</string>
|
||||
<string name="screen_media_upload_preview_title">"Lisää mediaa"</string>
|
||||
<string name="screen_report_content_block_user">"Estä käyttäjä"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Valitse tämä, jos haluat piilottaa kaikki nykyiset ja tulevat viestit tältä käyttäjältä"</string>
|
||||
<string name="screen_report_content_explanation">"Tämä viesti ilmoitetaan kotipalvelimesi ylläpitäjälle. Ylläpitäjä ei pysty lukemaan salattuja viestejä."</string>
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
<string name="emoji_picker_category_places">"Utazás és helyek"</string>
|
||||
<string name="emoji_picker_category_recent">"Legutóbbi emodzsik"</string>
|
||||
<string name="emoji_picker_category_symbols">"Szimbólumok"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Kép vízszintes tükrözése"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Vízszintesen tükrözve"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Eredeti"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Kép függőleges tükrözése"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Függőlegesen tükrözve"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Eredeti"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Kép elforgatása balra"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d fok"</item>
|
||||
<item quantity="other">"%1$d fok"</item>
|
||||
</plurals>
|
||||
<string name="screen_image_edition_title">"Fotó szerkesztése"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Koppintson a feltöltött videók minőségének módosításához"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"A fájl nem tölthető fel."</string>
|
||||
@@ -26,6 +38,7 @@
|
||||
<string name="screen_media_upload_preview_item_count">"%1$d. elem / %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Képminőség optimalizációja"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Feldolgozás…"</string>
|
||||
<string name="screen_media_upload_preview_title">"Média hozzáadása"</string>
|
||||
<string name="screen_report_content_block_user">"Felhasználó letiltása"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Jelölje be, ha el akarja rejteni az összes jelenlegi és jövőbeli üzenetet ettől a felhasználótól"</string>
|
||||
<string name="screen_report_content_explanation">"Ez az üzenet jelentve lesz a Matrix-kiszolgáló adminisztrátorának. Nem fogja tudni elolvasni a titkosított üzeneteket."</string>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<string name="screen_room_attachment_source_camera">"カメラ"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"写真を撮影"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"動画を撮影"</string>
|
||||
<string name="screen_room_attachment_source_files">"添付ファイル"</string>
|
||||
<string name="screen_room_attachment_source_files">"ファイルを添付"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"アルバムの写真と動画"</string>
|
||||
<string name="screen_room_attachment_source_location">"場所を共有"</string>
|
||||
<string name="screen_room_attachment_source_poll">"投票"</string>
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
<string name="emoji_picker_category_places">"여행 & 장소"</string>
|
||||
<string name="emoji_picker_category_recent">"최근 이모지"</string>
|
||||
<string name="emoji_picker_category_symbols">"상징"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"이미지를 왼쪽으로 회전"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="other">"%1$d도"</item>
|
||||
</plurals>
|
||||
<string name="screen_image_edition_title">"사진 편집"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"캡션은 오래된 앱을 사용하는 사용자에게 표시되지 않을 수 있습니다."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"비디오 업로드 품질을 변경하려면 탭하세요"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"파일을 업로드할 수 없습니다."</string>
|
||||
@@ -26,6 +31,7 @@
|
||||
<string name="screen_media_upload_preview_item_count">"전체 %2$d개 중 %1$d번째 파일"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"이미지 품질 최적화"</string>
|
||||
<string name="screen_media_upload_preview_processing">"처리 중…"</string>
|
||||
<string name="screen_media_upload_preview_title">"미디어 추가"</string>
|
||||
<string name="screen_report_content_block_user">"사용자 차단하기"</string>
|
||||
<string name="screen_report_content_block_user_hint">"이 사용자의 현재 및 향후 모든 메시지를 숨기려면 확인하세요."</string>
|
||||
<string name="screen_report_content_explanation">"이 메시지는 홈서버의 관리자에게 보고되었습니다. 암호화된 메시지는 읽을 수 없습니다."</string>
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
<string name="emoji_picker_category_places">"Podróż i miejsca"</string>
|
||||
<string name="emoji_picker_category_recent">"Ostatnie emoji"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbole"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Odwróć obraz w poziomie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Odwrócono w poziomie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Oryginalny"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Odwróć obraz w pionie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Odwrócono w pionie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Oryginalny"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Obróć obraz w lewo"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d stopień"</item>
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
<string name="emoji_picker_category_places">"文旅景点"</string>
|
||||
<string name="emoji_picker_category_recent">"最近的 Emoji"</string>
|
||||
<string name="emoji_picker_category_symbols">"符号"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻转图像"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻转"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"垂直翻转图像"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"垂直翻转"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"原始"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"向左旋转图像"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="other">"%1$d 度"</item>
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
<string name="emoji_picker_category_places">"Travel & Places"</string>
|
||||
<string name="emoji_picker_category_recent">"Recent emojis"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbols"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Flip image horizontally"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Flipped horizontally"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Flip image vertically"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Flipped vertically"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Original"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rotate the image to the left"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d degree"</item>
|
||||
|
||||
+3
@@ -23,6 +23,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider
|
||||
import io.element.android.features.messages.impl.timeline.createTimelineController
|
||||
import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint
|
||||
import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -42,6 +43,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -85,6 +87,7 @@ class DefaultMessagesEntryPointTest {
|
||||
knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
hasVulkanSupport = DeviceHasVulkanSupport(mockk(relaxed = true))
|
||||
)
|
||||
}
|
||||
val callback = object : MessagesEntryPoint.Callback {
|
||||
|
||||
-1
@@ -238,7 +238,6 @@ class MessagesViewTest {
|
||||
val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvent>()
|
||||
val state = aMessagesState(
|
||||
timelineState = aTimelineState(
|
||||
renderReadReceipts = true,
|
||||
timelineItems = persistentListOf(
|
||||
aTimelineItemEvent(
|
||||
readReceiptState = aTimelineItemReadReceipts(
|
||||
|
||||
+33
-2
@@ -624,7 +624,11 @@ class AttachmentsPreviewPresenterTest {
|
||||
val croppedState = awaitItem()
|
||||
croppedState.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft)
|
||||
val rotatedState = awaitItem()
|
||||
rotatedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits)
|
||||
rotatedState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally)
|
||||
val flippedHorizontallyState = awaitItem()
|
||||
flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically)
|
||||
val flippedState = awaitItem()
|
||||
flippedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits)
|
||||
|
||||
val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last()
|
||||
assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri)
|
||||
@@ -638,9 +642,36 @@ class AttachmentsPreviewPresenterTest {
|
||||
right = cropRect.bottom,
|
||||
bottom = 1f - cropRect.left,
|
||||
)
|
||||
reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(rotatedCropRect)
|
||||
val flippedCropRect = NormalizedCropRect(
|
||||
left = 1f - rotatedCropRect.right,
|
||||
top = 1f - rotatedCropRect.bottom,
|
||||
right = 1f - rotatedCropRect.left,
|
||||
bottom = 1f - rotatedCropRect.top,
|
||||
)
|
||||
reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(flippedCropRect)
|
||||
assertThat(reopenedState.imageEditorState.edits.rotationQuarterTurns).isEqualTo(3)
|
||||
assertThat(reopenedState.imageEditorState.edits.rotationDegrees).isEqualTo(270)
|
||||
assertThat(reopenedState.imageEditorState.edits.isFlippedHorizontally).isTrue()
|
||||
assertThat(reopenedState.imageEditorState.edits.isFlippedVertically).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - image editor flip events update edits`() = runTest {
|
||||
val presenter = createAttachmentsPreviewPresenter(displayMediaQualitySelectorViews = true)
|
||||
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor)
|
||||
val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last()
|
||||
|
||||
editorState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally)
|
||||
val flippedHorizontallyState = awaitItem()
|
||||
assertThat(flippedHorizontallyState.imageEditorState?.edits?.isFlippedHorizontally).isTrue()
|
||||
|
||||
flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically)
|
||||
val flippedState = awaitItem()
|
||||
assertThat(flippedState.imageEditorState?.edits?.isFlippedVertically).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -42,4 +42,37 @@ class AttachmentImageEditsTest {
|
||||
assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.8f)
|
||||
assertThat(result.hasChanges).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flip horizontally updates crop and change tracking`() {
|
||||
val sut = AttachmentImageEdits(
|
||||
cropRect = NormalizedCropRect(
|
||||
left = 0.1f,
|
||||
top = 0.3f,
|
||||
right = 0.6f,
|
||||
bottom = 0.9f,
|
||||
)
|
||||
)
|
||||
val result = sut.flipHorizontally()
|
||||
assertThat(result.isFlippedHorizontally).isTrue()
|
||||
assertThat(result.cropRect.left).isWithin(0.0001f).of(0.4f)
|
||||
assertThat(result.cropRect.right).isWithin(0.0001f).of(0.9f)
|
||||
assertThat(result.cropRect.top).isWithin(0.0001f).of(0.3f)
|
||||
assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.9f)
|
||||
assertThat(result.hasChanges).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flip vertical twice resets to default state`() {
|
||||
val edits = AttachmentImageEdits().flipVertically().flipVertically()
|
||||
assertThat(edits.isFlippedVertically).isFalse()
|
||||
assertThat(edits.hasChanges).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flip horizontally twice resets to default state`() {
|
||||
val edits = AttachmentImageEdits().flipHorizontally().flipHorizontally()
|
||||
assertThat(edits.isFlippedVertically).isFalse()
|
||||
assertThat(edits.hasChanges).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<string name="screen_labs_header_title">"Kokeilunhaluinen olo?"</string>
|
||||
<string name="screen_labs_title">"Labrat"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Lisäasetukset"</string>
|
||||
<string name="screen_notification_settings_call_ringtone_label">"Puhelun soittoääni"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Ääni- ja videopuheluista"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Konfiguraatio ei täsmää"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Olemme yksinkertaistaneet ilmoitusasetuksia, jotta vaihtoehdot olisi helpompi löytää. Joitakin aiemmin valitsemiasi asetuksia ei näytetä tässä, mutta ne ovat edelleen voimassa.
|
||||
@@ -76,17 +77,26 @@ Jos jatkat, jotkin asetukset saattavat muuttua."</string>
|
||||
<string name="screen_notification_settings_invite_for_me_label">"Kutsut"</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Kotipalvelimesi ei tue tätä vaihtoehtoa salatuissa huoneissa, joten et ehkä saa ilmoitusta joissakin huoneissa."</string>
|
||||
<string name="screen_notification_settings_mentions_section_title">"Maininnat"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Valitse toinen ääni…"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Käytössä tällä hetkellä %1$s"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_title">"Viestin ääni"</string>
|
||||
<string name="screen_notification_settings_message_sound_label">"Viestin ääni"</string>
|
||||
<string name="screen_notification_settings_mode_all">"Kaikki"</string>
|
||||
<string name="screen_notification_settings_mode_mentions">"Maininnat"</string>
|
||||
<string name="screen_notification_settings_notification_section_title">"Ilmoita minulle"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">"Ilmoita minulle @room-maininnoista"</string>
|
||||
<string name="screen_notification_settings_sound_custom_fallback">"Mukautettu"</string>
|
||||
<string name="screen_notification_settings_sound_custom_sound_button_title">"Mukautettu ääni…"</string>
|
||||
<string name="screen_notification_settings_sound_delete_sound_error_title">"Virhe tiedoston poistamisessa"</string>
|
||||
<string name="screen_notification_settings_sound_element_default">"Elementin oletus"</string>
|
||||
<string name="screen_notification_settings_sound_element_fade">"Element Fade"</string>
|
||||
<string name="screen_notification_settings_sound_import_sound_error_title">"Virhe tiedoston tuonnissa"</string>
|
||||
<string name="screen_notification_settings_sound_preview_sound_error_title">"Ongelma ilmoitusäänen esikatselussa"</string>
|
||||
<string name="screen_notification_settings_sound_section_title">"Ääni"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Hylkää hälytysäänen virhe"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_title">"Ongelma ilmoitusäänen asettamisessa"</string>
|
||||
<string name="screen_notification_settings_sound_silent">"Hiljainen"</string>
|
||||
<string name="screen_notification_settings_sound_system_default">"Järjestelmän oletus"</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required">"Jos haluat saada ilmoituksia, vaihda %1$s."</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"järjestelmäsi asetuksia"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"Järjestelmän ilmoitukset on poissa päältä"</string>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<string name="screen_labs_header_title">"Kísérletezni szeretne?"</string>
|
||||
<string name="screen_labs_title">"Kísérletek"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"További beállítások"</string>
|
||||
<string name="screen_notification_settings_call_ringtone_label">"Csengőhang"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Hang- és videóhívások"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Konfigurációs eltérés"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"Egyszerűsítettük az értesítési beállításokat, hogy könnyebben megtalálhatók legyenek a lehetőségek. A korábban kiválasztott egyéni beállítások némelyike nem jelenik meg itt, de továbbra is aktív.
|
||||
@@ -76,10 +77,52 @@ Ha folytatja, egyes beállítások megváltozhatnak."</string>
|
||||
<string name="screen_notification_settings_invite_for_me_label">"Meghívók"</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string>
|
||||
<string name="screen_notification_settings_mentions_section_title">"Említések"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Válasszon egy másik hangot…"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Jelenleg használt: %1$s"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_title">"Üzenet hang"</string>
|
||||
<string name="screen_notification_settings_message_sound_label">"Üzenet hang"</string>
|
||||
<string name="screen_notification_settings_mode_all">"Összes"</string>
|
||||
<string name="screen_notification_settings_mode_mentions">"Említések"</string>
|
||||
<string name="screen_notification_settings_notification_section_title">"Értesítés ezekről:"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">"Értesítés a @room említésekor"</string>
|
||||
<string name="screen_notification_settings_sound_custom_fallback">"Egyedi"</string>
|
||||
<string name="screen_notification_settings_sound_custom_sound_button_title">"Egyedi hang…"</string>
|
||||
<string name="screen_notification_settings_sound_delete_sound_error_title">"Hiba a fájl törlésekor"</string>
|
||||
<string name="screen_notification_settings_sound_element_default">"Element alapértelmezett"</string>
|
||||
<string name="screen_notification_settings_sound_element_fade">"Element Fade"</string>
|
||||
<string name="screen_notification_settings_sound_import_sound_error_title">"Hiba a fájl importálásakor"</string>
|
||||
<string name="screen_notification_settings_sound_preview_sound_error_title">"Probléma a riasztási hang előnézetekor"</string>
|
||||
<string name="screen_notification_settings_sound_section_title">"Hang"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Figyelmeztető hang hiba kikapcsolása"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_title">"Probléma a riasztási hang beállításánál"</string>
|
||||
<string name="screen_notification_settings_sound_silent">"Néma"</string>
|
||||
<string name="screen_notification_settings_sound_system_alert">"Riasztás"</string>
|
||||
<string name="screen_notification_settings_sound_system_anticipate">"Előrejelzés"</string>
|
||||
<string name="screen_notification_settings_sound_system_bell">"Harang"</string>
|
||||
<string name="screen_notification_settings_sound_system_bloom">"Virágzás"</string>
|
||||
<string name="screen_notification_settings_sound_system_calypso">"Calypso"</string>
|
||||
<string name="screen_notification_settings_sound_system_chime">"Chime"</string>
|
||||
<string name="screen_notification_settings_sound_system_choo_choo">"Choo Choo"</string>
|
||||
<string name="screen_notification_settings_sound_system_default">"Rendszer alapértelmezett"</string>
|
||||
<string name="screen_notification_settings_sound_system_descent">"Lejtő"</string>
|
||||
<string name="screen_notification_settings_sound_system_electronic">"Elektronikus"</string>
|
||||
<string name="screen_notification_settings_sound_system_fanfare">"Harsonaszó"</string>
|
||||
<string name="screen_notification_settings_sound_system_glass">"Üveg"</string>
|
||||
<string name="screen_notification_settings_sound_system_horn">"Kürt"</string>
|
||||
<string name="screen_notification_settings_sound_system_ladder">"Létra"</string>
|
||||
<string name="screen_notification_settings_sound_system_minuet">"Menüett"</string>
|
||||
<string name="screen_notification_settings_sound_system_news_flash">"Hírvillám"</string>
|
||||
<string name="screen_notification_settings_sound_system_noir">"Noir"</string>
|
||||
<string name="screen_notification_settings_sound_system_sherwood_forest">"Sherwood-erdő"</string>
|
||||
<string name="screen_notification_settings_sound_system_spell">"Varázslat"</string>
|
||||
<string name="screen_notification_settings_sound_system_suspense">"Feszültség"</string>
|
||||
<string name="screen_notification_settings_sound_system_swish">"Swish"</string>
|
||||
<string name="screen_notification_settings_sound_system_telegraph">"Távíró"</string>
|
||||
<string name="screen_notification_settings_sound_system_tiptoes">"Lábujjhegy"</string>
|
||||
<string name="screen_notification_settings_sound_system_tri_tone">"Három hang"</string>
|
||||
<string name="screen_notification_settings_sound_system_tweet">"Csipog"</string>
|
||||
<string name="screen_notification_settings_sound_system_typewriters">"Írógép"</string>
|
||||
<string name="screen_notification_settings_sound_system_update">"Frissítés"</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required">"Az értesítések fogadásához kérjük, módosítsa a %1$s."</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
|
||||
|
||||
@@ -54,9 +54,10 @@
|
||||
<string name="screen_labs_enable_threads">"スレッドへの返信を有効化"</string>
|
||||
<string name="screen_labs_enable_threads_description">"変更を適用するためにアプリケーションは再起動します。"</string>
|
||||
<string name="screen_labs_header_description">"開発段階の最新機能を試すことができます。未完成のため、変更や不安定な挙動が生じる可能性があります。"</string>
|
||||
<string name="screen_labs_header_title">"探究したいですか?"</string>
|
||||
<string name="screen_labs_header_title">"探究的な気分ですか?"</string>
|
||||
<string name="screen_labs_title">"ラボ"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"追加設定"</string>
|
||||
<string name="screen_notification_settings_call_ringtone_label">"発信音"</string>
|
||||
<string name="screen_notification_settings_calls_label">"音声・ビデオ通話"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"設定の不一致"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"通知設定を簡素化し、見つけやすくしました。以前のカスタム設定の一部はここに表示されませんが、引き続き有効です。
|
||||
@@ -75,10 +76,15 @@
|
||||
<string name="screen_notification_settings_invite_for_me_label">"招待"</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。"</string>
|
||||
<string name="screen_notification_settings_mentions_section_title">"メンション"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_choose_other">"他の音を選択…"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"%1$s を使用中"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_title">"メッセージの音"</string>
|
||||
<string name="screen_notification_settings_message_sound_label">"メッセージの音"</string>
|
||||
<string name="screen_notification_settings_mode_all">"すべて"</string>
|
||||
<string name="screen_notification_settings_mode_mentions">"メンション"</string>
|
||||
<string name="screen_notification_settings_notification_section_title">"以下を通知"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">@ルームで通知を受け取る</string>
|
||||
<string name="screen_notification_settings_sound_custom_fallback">"カスタム"</string>
|
||||
<string name="screen_notification_settings_sound_custom_sound_button_title">"カスタム着信音"</string>
|
||||
<string name="screen_notification_settings_sound_delete_sound_error_title">"ファイルの削除に失敗"</string>
|
||||
<string name="screen_notification_settings_sound_element_default">"Element の既定"</string>
|
||||
@@ -86,7 +92,9 @@
|
||||
<string name="screen_notification_settings_sound_import_sound_error_title">"ファイルの取り込みに失敗"</string>
|
||||
<string name="screen_notification_settings_sound_preview_sound_error_title">"通知音のプレビューで問題が発生しました"</string>
|
||||
<string name="screen_notification_settings_sound_section_title">"音"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"通知音のエラーを無視"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_title">"通知音の設定に問題が発生しました"</string>
|
||||
<string name="screen_notification_settings_sound_silent">"サイレント"</string>
|
||||
<string name="screen_notification_settings_sound_system_alert">"アラート"</string>
|
||||
<string name="screen_notification_settings_sound_system_anticipate">"予感"</string>
|
||||
<string name="screen_notification_settings_sound_system_bell">"ベル"</string>
|
||||
@@ -94,6 +102,7 @@
|
||||
<string name="screen_notification_settings_sound_system_calypso">"カリプソ"</string>
|
||||
<string name="screen_notification_settings_sound_system_chime">"チャイム"</string>
|
||||
<string name="screen_notification_settings_sound_system_choo_choo">"汽車ポッポ"</string>
|
||||
<string name="screen_notification_settings_sound_system_default">"システムのデフォルト"</string>
|
||||
<string name="screen_notification_settings_sound_system_descent">"下降"</string>
|
||||
<string name="screen_notification_settings_sound_system_electronic">"エレクトロニック"</string>
|
||||
<string name="screen_notification_settings_sound_system_fanfare">"ファンファーレ"</string>
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"방 초대 요청에서 아바타 숨기기"</string>
|
||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"타임라인에서 미디어 미리 보기 숨기기"</string>
|
||||
<string name="screen_advanced_settings_labs">"실험실"</string>
|
||||
<string name="screen_advanced_settings_live_location_section_description">"위치 업데이트 전환 기준 거리"</string>
|
||||
<string name="screen_advanced_settings_live_location_section_footer">"이 앱에 \"정확한 위치\" 권한이 허용되어 있는지 확인해 주세요. 권한을 변경하려면 %1$s(으)로 이동하세요."</string>
|
||||
<string name="screen_advanced_settings_live_location_section_footer_link">"앱 설정"</string>
|
||||
<string name="screen_advanced_settings_live_location_section_title">"실시간 위치 업데이트"</string>
|
||||
<plurals name="screen_advanced_settings_live_location_update_distance">
|
||||
<item quantity="other">"%1$d미터마다"</item>
|
||||
</plurals>
|
||||
<string name="screen_advanced_settings_media_compression_description">"사진과 동영상을 더 빠르게 업로드하고 데이터 사용량을 줄이세요"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"미디어 품질 최적화"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"중재와 안전"</string>
|
||||
@@ -51,6 +58,7 @@
|
||||
<string name="screen_labs_header_title">"새로운 것을 시도해보고 싶으신가요?"</string>
|
||||
<string name="screen_labs_title">"실험실"</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"추가 설정"</string>
|
||||
<string name="screen_notification_settings_call_ringtone_label">"통화 벨소리"</string>
|
||||
<string name="screen_notification_settings_calls_label">"음성 및 동영상 통화"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"구성 불일치"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"알림 설정을 간소화하여 옵션을 더 쉽게 찾을 수 있도록 했습니다. 과거에 선택한 일부 맞춤 설정은 여기에서 표시되지 않지만, 여전히 활성화되어 있습니다.
|
||||
@@ -69,14 +77,57 @@
|
||||
<string name="screen_notification_settings_invite_for_me_label">"초대"</string>
|
||||
<string name="screen_notification_settings_mentions_only_disclaimer">"귀하의 홈서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 일부 방에서는 알림이 표시되지 않을 수 있습니다."</string>
|
||||
<string name="screen_notification_settings_mentions_section_title">"언급"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_choose_other">"다른 소리를 선택하세요…"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"현재 사용 중: %1$s"</string>
|
||||
<string name="screen_notification_settings_message_sound_dialog_title">"메시지 소리"</string>
|
||||
<string name="screen_notification_settings_message_sound_label">"메시지 소리"</string>
|
||||
<string name="screen_notification_settings_mode_all">"모두"</string>
|
||||
<string name="screen_notification_settings_mode_mentions">"언급"</string>
|
||||
<string name="screen_notification_settings_notification_section_title">"나에게 알려주세요"</string>
|
||||
<string name="screen_notification_settings_room_mention_label">@room 에서 알림 받기</string>
|
||||
<string name="screen_notification_settings_sound_custom_fallback">"사용자 지정"</string>
|
||||
<string name="screen_notification_settings_sound_custom_sound_button_title">"소리 사용자 지정"</string>
|
||||
<string name="screen_notification_settings_sound_delete_sound_error_title">"파일 삭제 오류"</string>
|
||||
<string name="screen_notification_settings_sound_element_default">"Element 기본 설정"</string>
|
||||
<string name="screen_notification_settings_sound_element_fade">"엘리먼트 페이드"</string>
|
||||
<string name="screen_notification_settings_sound_import_sound_error_title">"파일 가져오기 오류"</string>
|
||||
<string name="screen_notification_settings_sound_preview_sound_error_title">"알림음 미리듣기 문제 발생"</string>
|
||||
<string name="screen_notification_settings_sound_section_title">"소리"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"알림음 오류 해제"</string>
|
||||
<string name="screen_notification_settings_sound_set_sound_error_title">"알림 소리 설정 문제"</string>
|
||||
<string name="screen_notification_settings_sound_silent">"무음"</string>
|
||||
<string name="screen_notification_settings_sound_system_alert">"알림"</string>
|
||||
<string name="screen_notification_settings_sound_system_anticipate">"앤티시페이트"</string>
|
||||
<string name="screen_notification_settings_sound_system_bell">"벨"</string>
|
||||
<string name="screen_notification_settings_sound_system_bloom">"블룸"</string>
|
||||
<string name="screen_notification_settings_sound_system_calypso">"칼립소"</string>
|
||||
<string name="screen_notification_settings_sound_system_chime">"차임"</string>
|
||||
<string name="screen_notification_settings_sound_system_choo_choo">"칙칙폭폭"</string>
|
||||
<string name="screen_notification_settings_sound_system_default">"시스템 기본값"</string>
|
||||
<string name="screen_notification_settings_sound_system_descent">"디센트"</string>
|
||||
<string name="screen_notification_settings_sound_system_electronic">"일렉트로닉"</string>
|
||||
<string name="screen_notification_settings_sound_system_fanfare">"팡파레"</string>
|
||||
<string name="screen_notification_settings_sound_system_glass">"글래스"</string>
|
||||
<string name="screen_notification_settings_sound_system_horn">"혼"</string>
|
||||
<string name="screen_notification_settings_sound_system_ladder">"래더"</string>
|
||||
<string name="screen_notification_settings_sound_system_minuet">"미뉴에트"</string>
|
||||
<string name="screen_notification_settings_sound_system_news_flash">"뉴스 플래시"</string>
|
||||
<string name="screen_notification_settings_sound_system_noir">"느와르"</string>
|
||||
<string name="screen_notification_settings_sound_system_sherwood_forest">"셔우드 포레스트"</string>
|
||||
<string name="screen_notification_settings_sound_system_spell">"스펠"</string>
|
||||
<string name="screen_notification_settings_sound_system_suspense">"서스펜스"</string>
|
||||
<string name="screen_notification_settings_sound_system_swish">"스위시"</string>
|
||||
<string name="screen_notification_settings_sound_system_telegraph">"텔레그래프"</string>
|
||||
<string name="screen_notification_settings_sound_system_tiptoes">"팁토즈"</string>
|
||||
<string name="screen_notification_settings_sound_system_tri_tone">"트라이톤"</string>
|
||||
<string name="screen_notification_settings_sound_system_tweet">"트윗"</string>
|
||||
<string name="screen_notification_settings_sound_system_typewriters">"타자기"</string>
|
||||
<string name="screen_notification_settings_sound_system_update">"업데이트"</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required">"알림을 받으려면 %1$s 을 변경해 주세요."</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"시스템 설정"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"시스템 알림이 꺼져 있습니다."</string>
|
||||
<string name="screen_notification_settings_title">"알림"</string>
|
||||
<string name="theme_black">"검은색"</string>
|
||||
<string name="theme_dark">"다크"</string>
|
||||
<string name="theme_light">"라이트"</string>
|
||||
<string name="theme_system">"시스템"</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_bug_report_editor_placeholder">"Kuvaile ongelmaasi…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Jos mahdollista, kirjoita englanniksi."</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"Kuvaus on liian lyhyt. Kerro tarkemmin mitä tapahtui, kiitos!"</string>
|
||||
<string name="screen_bug_report_github_issue_description">"Voit kirjoittaa mahdollisen GitHub-issuen numeron."</string>
|
||||
<string name="screen_bug_report_github_issue_label">"GitHub-issue"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Lähetä kaatumislokit"</string>
|
||||
<string name="screen_bug_report_include_logs">"Lähetä lokitiedostot"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"Lokitiedostosi ovat liian suuria, joten niitä ei voida sisällyttää tähän raporttiin. Lähetä ne meille toisella tavalla."</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_bug_report_editor_placeholder">"Írja le a problémát…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Ha lehetséges, a leírást angolul írja meg."</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"A leírás túl rövid, adjon meg további részleteket a történtekről. Köszönjük!"</string>
|
||||
<string name="screen_bug_report_github_issue_description">"Megadhatja egy kapcsolódó GitHub-probléma számát, ha van ilyen."</string>
|
||||
<string name="screen_bug_report_github_issue_label">"GitHub-jegy"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Összeomlásnaplók küldése"</string>
|
||||
<string name="screen_bug_report_include_logs">"Naplók engedélyezése"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"A naplófájlok túl nagyok, ezért nem szerepelhetnek ebben a jelentésben. Más módon küldje el őket."</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_bug_report_editor_placeholder">"問題の説明…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"可能であれば英語で記入してください。"</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"説明が過度に短いです。問題についてより詳細にご記入ください。"</string>
|
||||
<string name="screen_bug_report_github_issue_description">"関連したGitHubのissueがある場合はIDを記入してください。"</string>
|
||||
<string name="screen_bug_report_github_issue_label">"GitHub issue"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"クラッシュログを送信"</string>
|
||||
<string name="screen_bug_report_include_logs">"ログの記録を許可"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"ログのサイズが大きく、報告に添付することができません。ログは別の方法で送信してください。"</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_bug_report_editor_placeholder">"문제를 설명해 주세요…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"가능하다면 영어로 설명을 작성해 주십시오."</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"설명 내용이 너무 짧습니다. 발생한 상황에 대해 더 자세한 내용을 제공해 주시기 바랍니다. 감사합니다!"</string>
|
||||
<string name="screen_bug_report_github_issue_description">"연관된 GitHub 이슈 번호가 있다면 입력할 수 있습니다."</string>
|
||||
<string name="screen_bug_report_github_issue_label">"GitHub 이슈"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"충돌 로그 보내기"</string>
|
||||
<string name="screen_bug_report_include_logs">"로그 허용"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"귀하의 로그가 너무 커서 이 보고서에 포함할 수 없습니다. 다른 방법으로 보내주시기 바랍니다."</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="screen_bug_report_editor_placeholder">"Opisz problem…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Jeśli to możliwe, napisz zgłoszenje w języku angielskim."</string>
|
||||
<string name="screen_bug_report_error_description_too_short">"Opis jest zbyt krótki, podaj więcej szczegółów na temat tego co się stało. Dzięki!"</string>
|
||||
<string name="screen_bug_report_github_issue_description">"Możesz wpisać numer powiązanego zgłoszenia w serwisie GitHub, jeśli takie istnieje."</string>
|
||||
<string name="screen_bug_report_github_issue_label">"Zgłoszenie Github"</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Wyślij logi awarii"</string>
|
||||
<string name="screen_bug_report_include_logs">"Zezwól na logi"</string>
|
||||
<string name="screen_bug_report_include_logs_error">"Twoje dzienniki są zbyt duże, więc nie można ich uwzględnić w tym raporcie. Prześlij je do nas w inny sposób."</string>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<string name="screen_room_change_permissions_delete_messages">"메시지 삭제"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"멤버"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"사람 초대하기"</string>
|
||||
<string name="screen_room_change_permissions_live_location">"실시간 위치 공유"</string>
|
||||
<string name="screen_room_change_permissions_manage_space">"스페이스 관리"</string>
|
||||
<string name="screen_room_change_permissions_manage_space_rooms">"방 관리"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"멤버 관리"</string>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<string name="screen_chat_backup_key_backup_title">"鍵の保管庫"</string>
|
||||
<string name="screen_chat_backup_key_storage_disabled_error">"チャットをバックアップするには、鍵の保管庫を使用する必要があります。"</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_description">"この端末上の鍵をアップロードします"</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_title">"鍵の保管庫を使用します"</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_title">"鍵の保管庫を使用"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"回復鍵を変更"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change_description">"あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"回復鍵を入力"</string>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="screen_chat_backup_recovery_action_change_description">"Obnovte svoju kryptografickú totožnosť a históriu správ pomocou kľúča na obnovenie, ak ste stratili všetky svoje existujúce zariadenia."</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Zadajte kľúč na obnovenie"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Vaše úložisko kľúčov nie je momentálne synchronizované."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Nastaviť obnovenie"</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Získať kľúč na obnovenie"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_1">"Otvoriť %1$s v stolnom počítači"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_2">"Znova sa prihláste do svojho účtu"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_3">"Keď sa zobrazí výzva na overenie vášho zariadenia, vyberte %1$s"</string>
|
||||
@@ -25,10 +25,10 @@
|
||||
<string name="screen_encryption_reset_bullet_3">"Budete musieť znova overiť všetky existujúce zariadenia a kontakty"</string>
|
||||
<string name="screen_encryption_reset_footer">"Obnovte svoju totožnosť iba vtedy, ak nemáte prístup k inému prihlásenému zariadeniu a stratili ste kľúč na obnovenie."</string>
|
||||
<string name="screen_encryption_reset_title">"Znovu nastavte svoju totožnosť v prípade, že ju nemôžete potvrdiť iným spôsobom"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Vypnúť"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Odstrániť"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_description">"Ak odstránite všetky svoje zariadenia, stratíte svoju zašifrovanú históriu správ a budete si musieť obnoviť svoju digitálnu identitu."</string>
|
||||
<string name="screen_key_backup_disable_confirmation_title">"Ste si istí, že chcete vypnúť zálohovanie?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_title">"Naozaj chcete odstrániť úložisko kľúčov?"</string>
|
||||
<string name="screen_key_backup_disable_description">"Odstránením úložiska kľúčov sa zo servera odstránia vaše kľúče digitálnej identity a správ a vypnú sa nasledujúce bezpečnostné funkcie:"</string>
|
||||
<string name="screen_key_backup_disable_description_point_1">"Na nových zariadeniach nebudete mať zašifrovanú históriu správ"</string>
|
||||
<string name="screen_key_backup_disable_description_point_2">"Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite z aplikácie %1$s na všetkých zariadeniach"</string>
|
||||
<string name="screen_key_backup_disable_title">"Ste si istí, že chcete vypnúť zálohovanie?"</string>
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="screen_recovery_key_setup_generate_key">"Vygenerujte si váš kľúč na obnovenie"</string>
|
||||
<string name="screen_recovery_key_setup_generate_key_description">"Nezdieľajte to s nikým!"</string>
|
||||
<string name="screen_recovery_key_setup_success">"Úspešné nastavenie obnovy"</string>
|
||||
<string name="screen_recovery_key_setup_title">"Nastaviť obnovenie"</string>
|
||||
<string name="screen_recovery_key_setup_title">"Získať kľúč na obnovenie"</string>
|
||||
<string name="screen_reset_encryption_confirmation_alert_action">"Áno, znovu nastaviť teraz"</string>
|
||||
<string name="screen_reset_encryption_confirmation_alert_subtitle">"Tento proces je nezvratný."</string>
|
||||
<string name="screen_reset_encryption_confirmation_alert_title">"Naozaj chcete obnoviť svoje šifrovanie?"</string>
|
||||
|
||||
@@ -7,7 +7,7 @@ android_gradle_plugin = "8.13.2"
|
||||
# When updating this, please also update the version in the file ./idea/kotlinc.xml
|
||||
kotlin = "2.3.21"
|
||||
kotlinpoet = "2.3.0"
|
||||
ksp = "2.3.8"
|
||||
ksp = "2.3.9"
|
||||
firebaseAppDistribution = "5.2.1"
|
||||
|
||||
# AndroidX
|
||||
@@ -51,7 +51,7 @@ telephoto = "0.19.0"
|
||||
haze = "1.7.2"
|
||||
|
||||
# Dependency analysis
|
||||
dependencyAnalysis = "3.12.0"
|
||||
dependencyAnalysis = "3.14.0"
|
||||
|
||||
# DI
|
||||
metro = "1.1.1"
|
||||
@@ -84,7 +84,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
|
||||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.13.0"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.14.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
@@ -102,7 +102,7 @@ androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constr
|
||||
androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
|
||||
androidx_camera_view = { module = "androidx.camera:camera-view", version.ref = "camera" }
|
||||
androidx_camera_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
|
||||
androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.0.0"
|
||||
androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.1.0"
|
||||
androidx_workmanager_runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
|
||||
|
||||
androidx_recyclerview = "androidx.recyclerview:recyclerview:1.4.0"
|
||||
@@ -166,7 +166,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
|
||||
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||
test_junit = "junit:junit:4.13.2"
|
||||
test_runner = "androidx.test:runner:1.7.0"
|
||||
test_mockk = "io.mockk:mockk:1.14.9"
|
||||
test_mockk = "io.mockk:mockk:1.14.11"
|
||||
test_konsist = "com.lemonappdev:konsist:0.17.3"
|
||||
test_turbine = "app.cash.turbine:turbine:1.2.1"
|
||||
test_truth = "com.google.truth:truth:1.4.5"
|
||||
@@ -183,7 +183,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.26"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.06.3"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
@@ -226,10 +226,10 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
|
||||
color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.43.0"
|
||||
posthog = "com.posthog:posthog-android:3.47.0"
|
||||
sentry = "io.sentry:sentry-android:8.41.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.36.1"
|
||||
|
||||
# Emojibase
|
||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.5.3"
|
||||
@@ -239,7 +239,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
|
||||
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
||||
|
||||
# Element Call
|
||||
element_call_embedded = "io.element.android:element-call-embedded:0.20.0"
|
||||
element_call_embedded = "io.element.android:element-call-embedded:0.20.1"
|
||||
|
||||
# Auto services
|
||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.system
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
private const val VULKAN_VERSION_1_0 = 0x400003
|
||||
|
||||
/**
|
||||
* Checks if the device supports Vulkan 1.0.
|
||||
*
|
||||
* This is needed for the location screens that contain maps using MapLibre UI components.
|
||||
*
|
||||
* Needed until https://github.com/maplibre/maplibre-native/issues/3079 is resolved and we can automatically choose between OpenGL and Vulkan renderers,
|
||||
* or no devices support OpenGL anymore.
|
||||
*/
|
||||
@Inject
|
||||
class DeviceHasVulkanSupport(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
operator fun invoke(): Boolean {
|
||||
return context.packageManager.hasSystemFeature("android.hardware.vulkan.version", VULKAN_VERSION_1_0)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7901fea2f578c8ed796160c9c08f417c61f4fb21580f958844fdf0cb794adf8a
|
||||
size 239731
|
||||
oid sha256:78386a58e91be3213485f60909ec8d83cd535aabcec775aadf7b36e77d8542f0
|
||||
size 245853
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:245f012d419817f6557d92a71729b3b70092f24f0eba37f2f1fc431ad27592be
|
||||
size 252969
|
||||
oid sha256:477ab18ffa1ca81ca5e4c9f30f4466b19e14fcaddf641fc860c6be19a0e33d4f
|
||||
size 259185
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user