mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-06 22:13:31 +01:00
Merge pull request #6655 from vector-im/feature/eric/app-layout-toolbar
New App Layout Toolbar
This commit is contained in:
commit
e2ed4b4ae1
1
changelog.d/6655.feature
Normal file
1
changelog.d/6655.feature
Normal file
@ -0,0 +1 @@
|
||||
Adds new app layout toolbar (feature flagged)
|
@ -0,0 +1,4 @@
|
||||
<vector android:height="22dp" android:viewportHeight="22"
|
||||
android:viewportWidth="22" android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#737D8C" android:fillType="evenOdd" android:pathData="M16.999,14.899C18.07,13.407 18.7,11.577 18.7,9.6C18.7,4.574 14.626,0.5 9.6,0.5C4.574,0.5 0.5,4.574 0.5,9.6C0.5,14.626 4.574,18.7 9.6,18.7C11.577,18.7 13.406,18.07 14.899,16.999C14.941,17.055 14.988,17.109 15.039,17.161L18.939,21.061C19.525,21.646 20.475,21.646 21.06,21.061C21.646,20.475 21.646,19.525 21.06,18.939L17.16,15.039C17.109,14.988 17.055,14.941 16.999,14.899ZM15.7,9.6C15.7,12.969 12.969,15.7 9.6,15.7C6.231,15.7 3.5,12.969 3.5,9.6C3.5,6.231 6.231,3.5 9.6,3.5C12.969,3.5 15.7,6.231 15.7,9.6Z"/>
|
||||
</vector>
|
@ -71,4 +71,7 @@
|
||||
<dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen>
|
||||
<dimen name="location_sharing_live_duration_choice_margin_horizontal">12dp</dimen>
|
||||
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
|
||||
|
||||
<!-- Material 3 -->
|
||||
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
|
||||
</resources>
|
||||
|
@ -39,4 +39,14 @@
|
||||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
<!-- Material 3 -->
|
||||
|
||||
<style name="Widget.Vector.Material3.Toolbar" parent="Widget.Material3.Toolbar" />
|
||||
|
||||
<style name="Widget.Vector.Material3.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium">
|
||||
<item name="expandedTitleTextAppearance">@style/TextAppearance.Vector.Title.Medium</item>
|
||||
<item name="expandedTitleMarginBottom">20dp</item>
|
||||
<item name="collapsedTitleTextAppearance">@style/TextAppearance.Vector.Headline.Bold</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -32,6 +32,15 @@
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Headline.Bold" parent="TextAppearance.MaterialComponents.Headline1">
|
||||
<item name="fontFamily">sans-serif</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">@dimen/text_size_headline</item>
|
||||
<item name="android:letterSpacing">0</item>
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1">
|
||||
<item name="fontFamily">sans-serif</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
|
@ -25,4 +25,4 @@
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -149,6 +149,9 @@
|
||||
|
||||
<!-- Location sharing -->
|
||||
<item name="vctr_live_location">@color/vctr_live_location_dark</item>
|
||||
|
||||
<!-- Material 3 -->
|
||||
<item name="collapsingToolbarLayoutMediumSize">@dimen/collapsing_toolbar_layout_medium_size</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||
|
@ -150,8 +150,12 @@
|
||||
|
||||
<!-- Location sharing -->
|
||||
<item name="vctr_live_location">@color/vctr_live_location_light</item>
|
||||
|
||||
<!-- Material 3 -->
|
||||
<item name="collapsingToolbarLayoutMediumSize">@dimen/collapsing_toolbar_layout_medium_size</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||
|
||||
</resources>
|
||||
|
||||
|
@ -57,6 +57,7 @@ import im.vector.app.features.discovery.change.SetIdentityServerFragment
|
||||
import im.vector.app.features.home.HomeDetailFragment
|
||||
import im.vector.app.features.home.HomeDrawerFragment
|
||||
import im.vector.app.features.home.LoadingFragment
|
||||
import im.vector.app.features.home.NewHomeDetailFragment
|
||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
||||
import im.vector.app.features.home.room.detail.TimelineFragment
|
||||
import im.vector.app.features.home.room.detail.search.SearchFragment
|
||||
@ -258,6 +259,11 @@ interface FragmentModule {
|
||||
@FragmentKey(HomeDetailFragment::class)
|
||||
fun bindHomeDetailFragment(fragment: HomeDetailFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(NewHomeDetailFragment::class)
|
||||
fun bindNewHomeDetailFragment(fragment: NewHomeDetailFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(EmojiSearchResultFragment::class)
|
||||
|
@ -28,7 +28,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.platform.SimpleTextWatcher
|
||||
|
||||
fun EditText.setupAsSearch(
|
||||
@DrawableRes searchIconRes: Int = R.drawable.ic_search,
|
||||
@DrawableRes searchIconRes: Int = R.drawable.ic_home_search,
|
||||
@DrawableRes clearIconRes: Int = R.drawable.ic_x_gray
|
||||
) {
|
||||
addTextChangedListener(object : SimpleTextWatcher() {
|
||||
|
@ -50,6 +50,7 @@ import androidx.viewbinding.ViewBinding
|
||||
import com.airbnb.mvrx.MavericksView
|
||||
import com.bumptech.glide.util.Util
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@ -72,6 +73,7 @@ import im.vector.app.core.utils.ToolbarConfig
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.MainActivityArgs
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import im.vector.app.features.analytics.AnalyticsTracker
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
@ -161,6 +163,9 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||
@Inject
|
||||
lateinit var fontScalePreferences: FontScalePreferences
|
||||
|
||||
@Inject
|
||||
lateinit var vectorFeatures: VectorFeatures
|
||||
|
||||
lateinit var navigator: Navigator
|
||||
private set
|
||||
private lateinit var fragmentFactory: FragmentFactory
|
||||
@ -253,6 +258,14 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
||||
|
||||
initUiAndData()
|
||||
|
||||
if (vectorFeatures.isNewAppLayoutEnabled()) {
|
||||
tryOrNull { // Add to XML theme when feature flag is removed
|
||||
val toolbarBackground = MaterialColors.getColor(views.root, R.attr.vctr_toolbar_background)
|
||||
window.statusBarColor = toolbarBackground
|
||||
window.navigationBarColor = toolbarBackground
|
||||
}
|
||||
}
|
||||
|
||||
val titleRes = getTitleRes()
|
||||
if (titleRes != -1) {
|
||||
supportActionBar?.let {
|
||||
|
@ -159,7 +159,7 @@ fun startInstallFromSourceIntent(context: Context, activityResultLauncher: Activ
|
||||
}
|
||||
|
||||
fun startSharePlainTextIntent(
|
||||
fragment: Fragment,
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
chooserTitle: String?,
|
||||
text: String,
|
||||
@ -182,10 +182,10 @@ fun startSharePlainTextIntent(
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent)
|
||||
} else {
|
||||
fragment.startActivity(intent)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
context.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment<Fr
|
||||
|
||||
dialog.findViewById<View>(R.id.keys_backup_setup_share)?.debouncedClicks {
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
|
||||
text = recoveryKey,
|
||||
|
@ -104,7 +104,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
|
||||
?: return@withState
|
||||
|
||||
startSharePlainTextIntent(
|
||||
this,
|
||||
requireContext(),
|
||||
copyStartForActivityResult,
|
||||
context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
|
||||
recoveryKey,
|
||||
|
@ -46,6 +46,7 @@ import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorMenuProvider
|
||||
import im.vector.app.core.pushers.PushersManager
|
||||
import im.vector.app.core.pushers.UnifiedPushHelper
|
||||
import im.vector.app.core.utils.startSharePlainTextIntent
|
||||
import im.vector.app.databinding.ActivityHomeBinding
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.MainActivityArgs
|
||||
@ -203,11 +204,16 @@ class HomeActivity :
|
||||
)
|
||||
}
|
||||
}
|
||||
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
|
||||
views.drawerLayout.addDrawerListener(drawerListener)
|
||||
if (isFirstCreation()) {
|
||||
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||
replaceFragment(views.homeDrawerFragmentContainer, HomeDrawerFragment::class.java)
|
||||
if (vectorFeatures.isNewAppLayoutEnabled()) {
|
||||
views.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
replaceFragment(views.homeDetailFragmentContainer, NewHomeDetailFragment::class.java)
|
||||
} else {
|
||||
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||
replaceFragment(views.homeDrawerFragmentContainer, HomeDrawerFragment::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
sharedActionViewModel
|
||||
@ -552,7 +558,7 @@ class HomeActivity :
|
||||
nightlyProxy.onHomeResumed()
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.home
|
||||
override fun getMenuRes() = if (vectorFeatures.isNewAppLayoutEnabled()) R.menu.menu_new_home else R.menu.menu_home
|
||||
|
||||
override fun handlePrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.menu_home_init_sync_legacy).isVisible = vectorPreferences.developerMode()
|
||||
@ -591,10 +597,29 @@ class HomeActivity :
|
||||
navigator.openSettings(this)
|
||||
true
|
||||
}
|
||||
R.id.menu_home_invite_friends -> {
|
||||
launchInviteFriends()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchInviteFriends() {
|
||||
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
|
||||
analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))
|
||||
val text = getString(R.string.invite_friends_text, permalink)
|
||||
|
||||
startSharePlainTextIntent(
|
||||
context = this,
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = getString(R.string.invite_friends),
|
||||
text = text,
|
||||
extraTitle = getString(R.string.invite_friends_rich_title)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
views.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
|
@ -102,7 +102,7 @@ class HomeDrawerFragment @Inject constructor(
|
||||
val text = getString(R.string.invite_friends_text, permalink)
|
||||
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = getString(R.string.invite_friends),
|
||||
text = text,
|
||||
|
@ -0,0 +1,462 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import im.vector.app.AppStateHandler
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.commitTransaction
|
||||
import im.vector.app.core.extensions.toMvRxBundle
|
||||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.platform.VectorMenuProvider
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.ui.views.CurrentCallsView
|
||||
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
|
||||
import im.vector.app.core.ui.views.KeysBackupBanner
|
||||
import im.vector.app.databinding.FragmentNewHomeDetailBinding
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.list.RoomListFragment
|
||||
import im.vector.app.features.home.room.list.RoomListParams
|
||||
import im.vector.app.features.popup.PopupAlertManager
|
||||
import im.vector.app.features.popup.VerificationVectorAlert
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.workers.signout.BannerState
|
||||
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewHomeDetailFragment @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val alertManager: PopupAlertManager,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val appStateHandler: AppStateHandler,
|
||||
private val session: Session,
|
||||
) : VectorBaseFragment<FragmentNewHomeDetailBinding>(),
|
||||
KeysBackupBanner.Delegate,
|
||||
CurrentCallsView.Callback,
|
||||
OnBackPressed,
|
||||
VectorMenuProvider {
|
||||
|
||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||
private val unreadMessagesSharedViewModel: UnreadMessagesSharedViewModel by activityViewModel()
|
||||
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
|
||||
|
||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel
|
||||
|
||||
private var hasUnreadRooms = false
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.room_list
|
||||
|
||||
override fun handleMenuItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_home_mark_all_as_read -> {
|
||||
viewModel.handle(HomeDetailAction.MarkAllRoomsRead)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun handlePrepareMenu(menu: Menu) {
|
||||
withState(viewModel) { state ->
|
||||
val isRoomList = state.currentTab is HomeTab.RoomList
|
||||
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentNewHomeDetailBinding {
|
||||
return FragmentNewHomeDetailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
|
||||
setupBottomNavigationView()
|
||||
setupToolbar()
|
||||
setupKeysBackupBanner()
|
||||
setupActiveCallView()
|
||||
|
||||
withState(viewModel) {
|
||||
// Update the navigation view if needed (for when we restore the tabs)
|
||||
views.bottomNavigationView.selectedItemId = it.currentTab.toMenuId()
|
||||
}
|
||||
|
||||
viewModel.onEach(HomeDetailViewState::selectedSpace) { selectedSpace ->
|
||||
onSpaceChange(selectedSpace)
|
||||
}
|
||||
|
||||
viewModel.onEach(HomeDetailViewState::currentTab) { currentTab ->
|
||||
updateUIForTab(currentTab)
|
||||
}
|
||||
|
||||
viewModel.onEach(HomeDetailViewState::showDialPadTab) { showDialPadTab ->
|
||||
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
|
||||
}
|
||||
|
||||
viewModel.observeViewEvents { viewEvent ->
|
||||
when (viewEvent) {
|
||||
HomeDetailViewEvents.CallStarted -> handleCallStarted()
|
||||
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
|
||||
HomeDetailViewEvents.Loading -> showLoadingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
unknownDeviceDetectorSharedViewModel.onEach { state ->
|
||||
state.unknownSessions.invoke()?.let { unknownDevices ->
|
||||
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
|
||||
val uid = "review_login"
|
||||
alertManager.cancelAlert(uid)
|
||||
val olderUnverified = unknownDevices.filter { !it.isNew }
|
||||
val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
|
||||
if (newest != null) {
|
||||
promptForNewUnknownDevices(uid, state, newest)
|
||||
} else if (olderUnverified.isNotEmpty()) {
|
||||
// In this case we prompt to go to settings to review logins
|
||||
promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedCallActionViewModel
|
||||
.liveKnownCalls
|
||||
.observe(viewLifecycleOwner) {
|
||||
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull()
|
||||
val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
|
||||
setCurrentSpace(previousSpaceId ?: parentSpaceId)
|
||||
}
|
||||
|
||||
private fun setCurrentSpace(spaceId: String?) {
|
||||
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
|
||||
}
|
||||
|
||||
private fun handleCallStarted() {
|
||||
dismissLoadingDialog()
|
||||
val fragmentTag = HomeTab.DialPad.toFragmentTag()
|
||||
(childFragmentManager.findFragmentByTag(fragmentTag) as? DialPadFragment)?.clear()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
currentCallsViewPresenter.unBind()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
|
||||
callManager.checkForProtocolsSupportIfNeeded()
|
||||
refreshSpaceState()
|
||||
}
|
||||
|
||||
private fun refreshSpaceState() {
|
||||
appStateHandler.getCurrentSpace()?.let {
|
||||
onSpaceChange(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
|
||||
val user = state.myMatrixItem
|
||||
alertManager.postVectorAlert(
|
||||
VerificationVectorAlert(
|
||||
uid = uid,
|
||||
title = getString(R.string.new_session),
|
||||
description = getString(R.string.verify_this_session, newest.displayName ?: newest.deviceId ?: ""),
|
||||
iconId = R.drawable.ic_shield_warning
|
||||
).apply {
|
||||
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
|
||||
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)
|
||||
?.navigator
|
||||
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
|
||||
)
|
||||
}
|
||||
dismissedAction = Runnable {
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
|
||||
val user = state.myMatrixItem
|
||||
alertManager.postVectorAlert(
|
||||
VerificationVectorAlert(
|
||||
uid = uid,
|
||||
title = getString(R.string.review_logins),
|
||||
description = getString(R.string.verify_other_sessions),
|
||||
iconId = R.drawable.ic_shield_warning
|
||||
).apply {
|
||||
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
|
||||
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
|
||||
contentAction = Runnable {
|
||||
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity ->
|
||||
// mark as ignored to avoid showing it again
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
|
||||
)
|
||||
activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
|
||||
}
|
||||
}
|
||||
dismissedAction = Runnable {
|
||||
unknownDeviceDetectorSharedViewModel.handle(
|
||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSpaceChange(spaceSummary: RoomSummary?) {
|
||||
// Reimplement in next PR
|
||||
println(spaceSummary)
|
||||
}
|
||||
|
||||
private fun setupKeysBackupBanner() {
|
||||
serverBackupStatusViewModel
|
||||
.onEach {
|
||||
when (val banState = it.bannerState.invoke()) {
|
||||
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
|
||||
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
|
||||
null,
|
||||
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
|
||||
}
|
||||
}
|
||||
views.homeKeysBackupBanner.delegate = this
|
||||
}
|
||||
|
||||
private fun setupActiveCallView() {
|
||||
currentCallsViewPresenter.bind(views.currentCallsView, this)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
setupToolbar(views.toolbar)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
session.userService().getUser(session.myUserId)?.let { user ->
|
||||
avatarRenderer.render(user.toMatrixItem(), views.avatar)
|
||||
}
|
||||
}
|
||||
|
||||
views.avatar.debouncedClicks {
|
||||
navigator.openSettings(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBottomNavigationView() {
|
||||
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
|
||||
views.bottomNavigationView.setOnItemSelectedListener {
|
||||
val tab = when (it.itemId) {
|
||||
R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
|
||||
R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
|
||||
R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS)
|
||||
else -> HomeTab.DialPad
|
||||
}
|
||||
viewModel.handle(HomeDetailAction.SwitchTab(tab))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUIForTab(tab: HomeTab) {
|
||||
views.bottomNavigationView.menu.findItem(tab.toMenuId()).isChecked = true
|
||||
updateSelectedFragment(tab)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun HomeTab.toFragmentTag() = "FRAGMENT_TAG_$this"
|
||||
|
||||
private fun updateSelectedFragment(tab: HomeTab) {
|
||||
val fragmentTag = tab.toFragmentTag()
|
||||
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
|
||||
childFragmentManager.commitTransaction {
|
||||
childFragmentManager.fragments
|
||||
.filter { it != fragmentToShow }
|
||||
.forEach {
|
||||
detach(it)
|
||||
}
|
||||
if (fragmentToShow == null) {
|
||||
when (tab) {
|
||||
is HomeTab.RoomList -> {
|
||||
val params = RoomListParams(tab.displayMode)
|
||||
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
|
||||
}
|
||||
is HomeTab.DialPad -> {
|
||||
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tab is HomeTab.DialPad) {
|
||||
(fragmentToShow as? DialPadFragment)?.applyCallback()
|
||||
}
|
||||
attach(fragmentToShow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDialPadFragment(): Fragment {
|
||||
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
|
||||
return (fragment as DialPadFragment).apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
||||
}
|
||||
applyCallback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
|
||||
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
|
||||
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
|
||||
if (wasVisible && !isVisible) {
|
||||
// As we hide it check if it's not the current item!
|
||||
withState(viewModel) {
|
||||
if (it.currentTab.toMenuId() == tabId) {
|
||||
viewModel.handle(HomeDetailAction.SwitchTab(HomeTab.RoomList(RoomListDisplayMode.PEOPLE)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* KeysBackupBanner Listener
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun setupKeysBackup() {
|
||||
navigator.openKeysBackupSetup(requireActivity(), false)
|
||||
}
|
||||
|
||||
override fun recoverKeysBackup() {
|
||||
navigator.openKeysBackupManager(requireActivity())
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
|
||||
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
|
||||
views.syncStateView.render(
|
||||
it.syncState,
|
||||
it.incrementalSyncRequestState,
|
||||
it.pushCounter,
|
||||
vectorPreferences.developerShowDebugInfo()
|
||||
)
|
||||
|
||||
hasUnreadRooms = it.hasUnreadMessages
|
||||
}
|
||||
|
||||
private fun BadgeDrawable.render(count: Int, highlight: Boolean) {
|
||||
isVisible = count > 0
|
||||
number = count
|
||||
maxCharacterCount = 3
|
||||
badgeTextColor = ThemeUtils.getColor(requireContext(), R.attr.colorOnPrimary)
|
||||
backgroundColor = if (highlight) {
|
||||
ThemeUtils.getColor(requireContext(), R.attr.colorError)
|
||||
} else {
|
||||
ThemeUtils.getColor(requireContext(), R.attr.vctr_unread_background)
|
||||
}
|
||||
}
|
||||
|
||||
private fun HomeTab.toMenuId() = when (this) {
|
||||
is HomeTab.DialPad -> R.id.bottom_action_dial_pad
|
||||
is HomeTab.RoomList -> when (displayMode) {
|
||||
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
|
||||
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
|
||||
else -> R.id.bottom_action_notification
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTapToReturnToCall() {
|
||||
callManager.getCurrentCall()?.let { call ->
|
||||
VectorCallActivity.newIntent(
|
||||
context = requireContext(),
|
||||
callId = call.callId,
|
||||
signalingRoomId = call.signalingRoomId,
|
||||
otherUserId = call.mxCall.opponentUserId,
|
||||
isIncomingCall = !call.mxCall.isOutgoing,
|
||||
isVideoCall = call.mxCall.isVideoCall,
|
||||
mode = null
|
||||
).let {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DialPadFragment.applyCallback(): DialPadFragment {
|
||||
callback = object : DialPadFragment.Callback {
|
||||
override fun onOkClicked(formatted: String?, raw: String?) {
|
||||
if (raw.isNullOrEmpty()) return
|
||||
viewModel.handle(HomeDetailAction.StartCallWithPhoneNumber(raw))
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean) = if (appStateHandler.getCurrentSpace() != null) {
|
||||
navigateBack()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
@ -334,7 +334,7 @@ class RoomMemberProfileFragment @Inject constructor(
|
||||
.setNeutralButton(R.string.ok, null)
|
||||
.setPositiveButton(R.string.share_by_text) { _, _ ->
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = null,
|
||||
text = permalink
|
||||
|
@ -337,7 +337,7 @@ class RoomProfileFragment @Inject constructor(
|
||||
|
||||
private fun onShareRoomProfile(permalink: String) {
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = null,
|
||||
text = permalink
|
||||
|
@ -89,7 +89,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
|
||||
}
|
||||
is ShareSpaceViewEvents.ShowInviteByLink -> {
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = getString(R.string.share_by_text),
|
||||
text = getString(R.string.share_space_link_message, event.spaceName, event.permalink),
|
||||
|
@ -67,7 +67,7 @@ class ShowUserCodeFragment @Inject constructor(
|
||||
sharedViewModel.observeViewEvents {
|
||||
if (it is UserCodeShareViewEvents.SharePlainText) {
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = it.title,
|
||||
text = it.text,
|
||||
|
@ -96,7 +96,7 @@ class UserListFragment @Inject constructor(
|
||||
is UserListViewEvents.OpenShareMatrixToLink -> {
|
||||
val text = getString(R.string.invite_friends_text, it.link)
|
||||
startSharePlainTextIntent(
|
||||
fragment = this,
|
||||
context = requireContext(),
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = getString(R.string.invite_friends),
|
||||
text = text,
|
||||
|
@ -17,4 +17,4 @@
|
||||
android:left="14dp"
|
||||
android:right="14dp"
|
||||
android:top="14dp" />
|
||||
</layer-list>
|
||||
</layer-list>
|
||||
|
103
vector/src/main/res/layout/fragment_new_home_detail.xml
Normal file
103
vector/src/main/res/layout/fragment_new_home_detail.xml
Normal file
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<im.vector.app.core.ui.views.KeysBackupBanner
|
||||
android:id="@+id/homeKeysBackupBanner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?vctr_keys_backup_banner_accent_color"
|
||||
android:minHeight="67dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/currentCallsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/homeKeysBackupBanner"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<im.vector.app.features.sync.widget.SyncStateView
|
||||
android:id="@+id/syncStateView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/currentCallsView"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/currentCallsView">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/syncStateView">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
style="@style/Widget.Vector.Material3.CollapsingToolbar.Medium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Widget.Vector.Material3.Toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="0dp"
|
||||
app:layout_collapseMode="pin"
|
||||
app:title="@string/all_chats">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:padding="6dp"
|
||||
android:contentDescription="@string/a11y_open_settings"
|
||||
tools:src="@sample/user_round_avatars" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/roomListContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottomNavigationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:menu="@menu/home_bottom_navigation" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:icon="@drawable/ic_home_search"
|
||||
app:iconTint="?vctr_content_primary"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="android.widget.SearchView"
|
||||
|
42
vector/src/main/res/menu/menu_new_home.xml
Normal file
42
vector/src/main/res/menu/menu_new_home.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_invite_friends"
|
||||
android:title="@string/invite_friends"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_suggestion"
|
||||
android:icon="@drawable/ic_material_bug_report"
|
||||
android:title="@string/send_suggestion"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_report_bug"
|
||||
android:icon="@drawable/ic_material_bug_report"
|
||||
android:title="@string/send_bug_report"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_init_sync_legacy"
|
||||
android:title="Do a legacy init sync"
|
||||
tools:ignore="HardcodedText"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_init_sync_optimized"
|
||||
android:title="Do an optimized init sync"
|
||||
tools:ignore="HardcodedText"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_filter"
|
||||
android:icon="@drawable/ic_home_search"
|
||||
android:title="@string/home_filter_placeholder_home"
|
||||
app:iconTint="?vctr_content_secondary"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
@ -136,6 +136,7 @@
|
||||
<string name="matrix_error">Matrix error</string>
|
||||
|
||||
<!-- Home Screen -->
|
||||
<string name="all_chats">All Chats</string>
|
||||
|
||||
<!-- Last seen time -->
|
||||
|
||||
@ -2808,6 +2809,7 @@
|
||||
|
||||
<string name="a11y_screenshot">Screenshot</string>
|
||||
<string name="a11y_open_widget">Open widgets</string>
|
||||
<string name="a11y_open_settings">Open settings</string>
|
||||
<string name="a11y_import_key_from_file">Import key from file</string>
|
||||
<string name="a11y_image">Image</string>
|
||||
<string name="a11y_change_avatar">Change avatar</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user