Merge branch 'develop' into feature/stabilization

This commit is contained in:
ganfra 2020-01-07 14:28:23 +01:00
commit 19fb3ce032
53 changed files with 1828 additions and 931 deletions

View file

@ -8,13 +8,19 @@ Improvements 🙌:
- The initial sync is now handled by a foreground service - The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline - Render aliases and canonical alias change in the timeline
- Fix autocompletion issues and add support for rooms and groups - Fix autocompletion issues and add support for rooms and groups
- Introduce developer mode in the settings (#796)
- Improve devices list screen
- Add settings for rageshake sensibility
- Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
Other changes: Other changes:
- -
Bugfix 🐛: Bugfix 🐛:
- Fix crash when opening room creation screen from the room filtering screen
- Fix avatar image disappearing (#777) - Fix avatar image disappearing (#777)
- Fix read marker banner when permalink - Fix read marker banner when permalink
- Hide non working settings (#751)
Translations 🗣: Translations 🗣:
- -

View file

@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4) ?.chunked(4)
?.joinToString(separator = " ") ?.joinToString(separator = " ")
fun MutableList<DeviceInfo>.sortByLastSeen() { /* ==========================================================================================
sortWith(DatedObjectComparators.descComparator) * DeviceInfo
* ========================================================================================== */
fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> {
val list = toMutableList()
list.sortWith(DatedObjectComparators.descComparator)
return list
} }

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
fun isRoomEncrypted(roomId: String): Boolean fun isRoomEncrypted(roomId: String): Boolean

View file

@ -123,6 +123,9 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds @Binds
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask

View file

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks // Tasks
private val getDevicesTask: GetDevicesTask, private val getDevicesTask: GetDevicesTask,
private val getDeviceInfoTask: GetDeviceInfoTask,
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
} }

View file

@ -25,11 +25,18 @@ internal interface CryptoApi {
/** /**
* Get the devices list * Get the devices list
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
fun getDevices(): Call<DevicesListResponse> fun getDevices(): Call<DevicesListResponse>
/**
* Get the device info by id
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
fun getDeviceInfo(@Path("deviceId") deviceId: String): Call<DeviceInfo>
/** /**
* Upload device and/or one-time keys. * Upload device and/or one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload

View file

@ -0,0 +1,37 @@
/*
* 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.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface GetDeviceInfoTask : Task<GetDeviceInfoTask.Params, DeviceInfo> {
data class Params(val deviceId: String)
}
internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi)
: GetDeviceInfoTask {
override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
return executeRequest {
apiCall = cryptoApi.getDeviceInfo(params.deviceId)
}
}
}

View file

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:name=".VectorApplication" android:name=".VectorApplication"

View file

@ -45,6 +45,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@ -228,6 +229,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsIgnoredUsersFragment::class) @FragmentKey(VectorSettingsIgnoredUsersFragment::class)
fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsDevicesFragment::class)
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(SASVerificationIncomingFragment::class) @FragmentKey(SASVerificationIncomingFragment::class)

View file

@ -0,0 +1,32 @@
/*
* Copyright 2020 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.riotx.core.hardware
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
fun vibrate(context: Context, durationMillis: Long = 100) {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(durationMillis)
}
}

View file

@ -54,6 +54,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ActivityOtherThemes
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.receivers.DebugReceiver import im.vector.riotx.receivers.DebugReceiver
@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var configurationViewModel: ConfigurationViewModel
private lateinit var sessionListener: SessionListener private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake lateinit var rageShake: RageShake
private set
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var activeSessionHolder: ActiveSessionHolder
private lateinit var vectorPreferences: VectorPreferences
// Filter for multiple invalid token error // Filter for multiple invalid token error
private var mainActivityStarted = false private var mainActivityStarted = false
@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this) val vectorComponent = getVectorComponent()
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
val timeForInjection = measureTimeMillis { val timeForInjection = measureTimeMillis {
injectWith(screenComponent) injectWith(screenComponent)
} }
@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
rageShake = screenComponent.rageShake() rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator() navigator = screenComponent.navigator()
activeSessionHolder = screenComponent.activeSessionHolder() activeSessionHolder = screenComponent.activeSessionHolder()
vectorPreferences = vectorComponent.vectorPreferences()
configurationViewModel.activityRestarter.observe(this, Observer { configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) { if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed // Recreate the Activity because configuration has changed
@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
configurationViewModel.onActivityResumed() configurationViewModel.onActivityResumed()
if (this !is BugReportActivity) { if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
rageShake.start() rageShake.start()
} }

View file

@ -1,36 +0,0 @@
/*
* Copyright 2018 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.riotx.core.preference
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import im.vector.riotx.R
/**
* Divider for Preference screen
*/
class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
init {
layoutResource = R.layout.vector_preference_divider
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.riotx.features.autocomplete.emoji
import android.graphics.Typeface
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.reactions.ReactionClickListener
import im.vector.riotx.features.reactions.data.EmojiItem
import javax.inject.Inject
class AutocompleteEmojiController @Inject constructor(
private val fontProvider: EmojiCompatFontProvider
) : TypedEpoxyController<List<EmojiItem>>() {
var emojiTypeface: Typeface? = fontProvider.typeface
private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener {
override fun compatibilityFontUpdate(typeface: Typeface?) {
emojiTypeface = typeface
}
}
init {
fontProvider.addListener(fontProviderListener)
}
var listener: AutocompleteClickListener<String>? = null
override fun buildModels(data: List<EmojiItem>?) {
if (data.isNullOrEmpty()) {
return
}
data
.take(MAX)
.forEach { emojiItem ->
autocompleteEmojiItem {
id(emojiItem.name)
emojiItem(emojiItem)
emojiTypeFace(emojiTypeface)
onClickListener(
object : ReactionClickListener {
override fun onReactionSelected(reaction: String) {
listener?.onItemClick(reaction)
}
}
)
}
}
if (data.size > MAX) {
autocompleteMoreResultItem {
id("more_result")
}
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
fontProvider.removeListener(fontProviderListener)
}
companion object {
const val MAX = 50
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.riotx.features.autocomplete.emoji
import android.graphics.Typeface
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.reactions.ReactionClickListener
import im.vector.riotx.features.reactions.data.EmojiItem
@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji)
abstract class AutocompleteEmojiItem : VectorEpoxyModel<AutocompleteEmojiItem.Holder>() {
@EpoxyAttribute
lateinit var emojiItem: EmojiItem
@EpoxyAttribute
var emojiTypeFace: Typeface? = null
@EpoxyAttribute
var onClickListener: ReactionClickListener? = null
override fun bind(holder: Holder) {
holder.emojiText.text = emojiItem.emoji
holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.emojiNameText.text = emojiItem.name
holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
holder.view.setOnClickListener {
onClickListener?.onReactionSelected(emojiItem.emoji)
}
}
class Holder : VectorEpoxyHolder() {
val emojiText by bind<TextView>(R.id.itemAutocompleteEmoji)
val emojiNameText by bind<TextView>(R.id.itemAutocompleteEmojiName)
val emojiKeywordText by bind<TextView>(R.id.itemAutocompleteEmojiSubname)
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.riotx.features.autocomplete.emoji
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.reactions.data.EmojiDataSource
import javax.inject.Inject
class AutocompleteEmojiPresenter @Inject constructor(context: Context,
private val emojiDataSource: EmojiDataSource,
private val controller: AutocompleteEmojiController) :
RecyclerViewPresenter<String>(context), AutocompleteClickListener<String> {
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: String) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
val data = if (query.isNullOrBlank()) {
// Return common emojis
emojiDataSource.getQuickReactions()
} else {
emojiDataSource.filterWith(query.toString())
}
controller.setData(data)
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.riotx.features.autocomplete.emoji
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result)
abstract class AutocompleteMoreResultItem : VectorEpoxyModel<AutocompleteMoreResultItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -0,0 +1,237 @@
/*
* 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.riotx.features.home.room.detail
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.Spannable
import android.widget.EditText
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
class AutoCompleter @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteMemberPresenter: AutocompleteMemberPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
) {
private lateinit var editText: EditText
fun enterSpecialMode() {
commandAutocompletePolicy.enabled = false
}
fun exitSpecialMode() {
commandAutocompletePolicy.enabled = true
}
private val glideRequests by lazy {
GlideApp.with(editText)
}
fun setup(editText: EditText, listener: AutoCompleterListener) {
this.editText = editText
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
setupCommands(backgroundDrawable, editText)
setupUsers(backgroundDrawable, editText, listener)
setupRooms(backgroundDrawable, editText, listener)
setupGroups(backgroundDrawable, editText, listener)
setupEmojis(backgroundDrawable, editText)
}
fun render(state: TextComposerViewState) {
autocompleteMemberPresenter.render(state.asyncMembers)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
}
private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on<Command>(editText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable.clear()
editable
.append(item.command)
.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteMemberPresenter.Callback) {
autocompleteMemberPresenter.callback = listener
Autocomplete.on<RoomMember>(editText)
.with(CharPolicy('@', true))
.with(autocompleteMemberPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomMember> {
override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) {
autocompleteRoomPresenter.callback = listener
Autocomplete.on<RoomSummary>(editText)
.with(CharPolicy('#', true))
.with(autocompleteRoomPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) {
autocompleteGroupPresenter.callback = listener
Autocomplete.on<GroupSummary>(editText)
.with(CharPolicy('+', true))
.with(autocompleteGroupPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
insertMatrixItem(editText, editable, "+", item.toMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on<String>(editText)
.with(CharPolicy(':', false))
.with(autocompleteEmojiPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<String> {
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
// Detect last ":" and remove it
var startIndex = editable.lastIndexOf(":")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
editable.replace(startIndex, endIndex, item)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
// Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
editText.context,
matrixItem
)
span.bind(editText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
interface AutoCompleterListener :
AutocompleteMemberPresenter.Callback,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback
companion object {
private const val ELEVATION = 6f
}
}

View file

@ -20,12 +20,10 @@ import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
@ -52,17 +50,11 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
@ -70,7 +62,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
@ -84,11 +75,6 @@ import im.vector.riotx.core.utils.*
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.attachments.ContactAttachment
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.getColorFromUserId
@ -117,7 +103,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor(
private val session: Session, private val session: Session,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val timelineEventController: TimelineEventController, private val timelineEventController: TimelineEventController,
private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autoCompleter: AutoCompleter,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteMemberPresenter: AutocompleteMemberPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor(
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteMemberPresenter.Callback, AutoCompleter.AutoCompleterListener,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -396,7 +375,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderRegularMode(text: String) { private fun renderRegularMode(text: String) {
commandAutocompletePolicy.enabled = true autoCompleter.exitSpecialMode()
composerLayout.collapse() composerLayout.collapse()
updateComposerText(text) updateComposerText(text)
@ -407,7 +386,7 @@ class RoomDetailFragment @Inject constructor(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
@StringRes descriptionRes: Int, @StringRes descriptionRes: Int,
defaultContent: String) { defaultContent: String) {
commandAutocompletePolicy.enabled = false autoCompleter.enterSpecialMode()
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.getDisambiguatedDisplayName()
@ -581,165 +560,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun setupComposer() { private fun setupComposer() {
val elevation = 6f autoCompleter.setup(composerLayout.composerEditText, this)
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
Autocomplete.on<Command>(composerLayout.composerEditText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable.clear()
editable
.append(item.command)
.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteRoomPresenter.callback = this
Autocomplete.on<RoomSummary>(composerLayout.composerEditText)
.with(CharPolicy('#', true))
.with(autocompleteRoomPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
// Detect last '#' and remove it
var startIndex = editable.lastIndexOf("#")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toRoomAliasMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteGroupPresenter.callback = this
Autocomplete.on<GroupSummary>(composerLayout.composerEditText)
.with(CharPolicy('+', true))
.with(autocompleteGroupPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
// Detect last '+' and remove it
var startIndex = editable.lastIndexOf("+")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteMemberPresenter.callback = this
Autocomplete.on<RoomMember>(composerLayout.composerEditText)
.with(CharPolicy('@', true))
.with(autocompleteMemberPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomMember> {
override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
// Detect last '@' and remove it
var startIndex = editable.lastIndexOf("@")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
composerLayout.callback = object : TextComposerView.Callback { composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) { if (!::attachmentTypeSelector.isInitialized) {
@ -836,9 +657,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderTextComposerState(state: TextComposerViewState) { private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteMemberPresenter.render(state.asyncMembers) autoCompleter.render(state)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
} }
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {

View file

@ -41,6 +41,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.settings.VectorPreferences
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -85,7 +87,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private val htmlCompressor: VectorHtmlCompressor, private val htmlCompressor: VectorHtmlCompressor,
private val session: Session, private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter, private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) { ) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) {
private val eventId = initialState.eventId private val eventId = initialState.eventId
@ -98,9 +101,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> { companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@ -143,7 +143,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
room.rx() room.rx()
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
quickEmojis.map { emoji -> EmojiDataSource.quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
} }
} }
@ -258,14 +258,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
} }
if (vectorPreferences.developerMode()) {
add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent())) add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
if (timelineEvent.isEncrypted()) { if (timelineEvent.isEncrypted()) {
val decryptedContent = timelineEvent.root.toClearContentStringWithIndent() val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error) ?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(EventSharedAction.ViewDecryptedSource(decryptedContent)) add(EventSharedAction.ViewDecryptedSource(decryptedContent))
} }
}
add(EventSharedAction.CopyPermalink(eventId)) add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != timelineEvent.root.senderId) { if (session.myUserId != timelineEvent.root.senderId) {
// not sent by me // not sent by me
if (timelineEvent.root.getClearType() == EventType.MESSAGE) { if (timelineEvent.root.getClearType() == EventType.MESSAGE) {

View file

@ -120,8 +120,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent) context.startActivity(intent)
} }
override fun openSettings(context: Context) { override fun openSettings(context: Context, directAccess: Int) {
val intent = VectorSettingsActivity.getIntent(context) val intent = VectorSettingsActivity.getIntent(context, directAccess)
context.startActivity(intent) context.startActivity(intent)
} }

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
interface Navigator { interface Navigator {
@ -39,7 +40,7 @@ interface Navigator {
fun openRoomsFiltering(context: Context) fun openRoomsFiltering(context: Context)
fun openSettings(context: Context) fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT)
fun openDebug(context: Context) fun openDebug(context: Context)

View file

@ -19,33 +19,32 @@ package im.vector.riotx.features.rageshake
import android.content.Context import android.content.Context
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.preference.PreferenceManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.squareup.seismic.ShakeDetector import com.squareup.seismic.ShakeDetector
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.hardware.vibrate
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity
import javax.inject.Inject import javax.inject.Inject
class RageShake @Inject constructor(private val activity: AppCompatActivity, class RageShake @Inject constructor(private val activity: AppCompatActivity,
private val bugReporter: BugReporter) : ShakeDetector.Listener { private val bugReporter: BugReporter,
private val navigator: Navigator,
private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener {
private var shakeDetector: ShakeDetector? = null private var shakeDetector: ShakeDetector? = null
private var dialogDisplayed = false private var dialogDisplayed = false
var interceptor: (() -> Unit)? = null
fun start() { fun start() {
if (!isEnable(activity)) { val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return
return
}
val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
if (sensorManager == null) {
return
}
shakeDetector = ShakeDetector(this).apply { shakeDetector = ShakeDetector(this).apply {
setSensitivity(vectorPreferences.getRageshakeSensitivity())
start(sensorManager) start(sensorManager)
} }
} }
@ -54,52 +53,43 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
shakeDetector?.stop() shakeDetector?.stop()
} }
/** fun setSensitivity(sensitivity: Int) {
* Enable the feature, and start it shakeDetector?.setSensitivity(sensitivity)
*/
fun enable() {
PreferenceManager.getDefaultSharedPreferences(activity).edit {
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
}
start()
}
/**
* Disable the feature, and stop it
*/
fun disable() {
PreferenceManager.getDefaultSharedPreferences(activity).edit {
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false)
}
stop()
} }
override fun hearShake() { override fun hearShake() {
val i = interceptor
if (i != null) {
vibrate(activity)
i.invoke()
} else {
if (dialogDisplayed) { if (dialogDisplayed) {
// Filtered! // Filtered!
return return
} }
vibrate(activity)
dialogDisplayed = true dialogDisplayed = true
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setMessage(R.string.send_bug_report_alert_message) .setMessage(R.string.send_bug_report_alert_message)
.setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() } .setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
.setNeutralButton(R.string.disable) { _, _ -> disable() } .setNeutralButton(R.string.settings) { _, _ -> openSettings() }
.setOnDismissListener { dialogDisplayed = false } .setOnDismissListener { dialogDisplayed = false }
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
.show() .show()
} }
}
private fun openBugReportScreen() { private fun openBugReportScreen() {
bugReporter.openBugReportScreen(activity) bugReporter.openBugReportScreen(activity)
} }
companion object { private fun openSettings() {
private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
}
companion object {
/** /**
* Check if the feature is available * Check if the feature is available
*/ */
@ -107,12 +97,5 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager) return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager)
?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
} }
/**
* Check if the feature is enable (enabled by default)
*/
private fun isEnable(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
}
} }
} }

View file

@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
} }
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
val words = action.queryString.split("\\s".toRegex())
setState { setState {
copy( copy(
query = action.queryString, query = action.queryString,
// First add emojis with name matching query, sorted by name results = dataSource.filterWith(action.queryString)
// Then emojis with keyword matching any of the word in the query, sorted by name
results = dataSource.rawData.emojis
.values
.filter { emojiItem ->
emojiItem.name.contains(action.queryString, true)
}
.sortedBy { it.name }
+ dataSource.rawData.emojis
.values
.filter { emojiItem ->
words.fold(true, { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
})
}
.sortedBy { it.name }
) )
} }
} }

View file

@ -33,4 +33,49 @@ class EmojiDataSource @Inject constructor(
.fromJson(input.bufferedReader().use { it.readText() }) .fromJson(input.bufferedReader().use { it.readText() })
} }
?: EmojiData(emptyList(), emptyMap(), emptyMap()) ?: EmojiData(emptyList(), emptyMap(), emptyMap())
private val quickReactions = mutableListOf<EmojiItem>()
fun filterWith(query: String): List<EmojiItem> {
val words = query.split("\\s".toRegex())
// First add emojis with name matching query, sorted by name
return (rawData.emojis.values
.filter { emojiItem ->
emojiItem.name.contains(query, true)
}
.sortedBy { it.name } +
// Then emojis with keyword matching any of the word in the query, sorted by name
rawData.emojis.values
.filter { emojiItem ->
words.fold(true, { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
})
}
.sortedBy { it.name })
// and ensure they will not be present twice
.distinct()
}
fun getQuickReactions(): List<EmojiItem> {
if (quickReactions.isEmpty()) {
listOf(
"+1", // 👍
"-1", // 👎
"grinning", // 😄
"tada", // 🎉
"confused", // 😕
"heart", // ❤️
"rocket", // 🚀
"eyes" // 👀
)
.mapNotNullTo(quickReactions) { rawData.emojis[it] }
}
return quickReactions
}
companion object {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
}
} }

View file

@ -22,6 +22,7 @@ import android.os.Bundle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
@ -45,6 +46,10 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
configureToolbar(toolbar) configureToolbar(toolbar)
} }
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun initUiAndData() { override fun initUiAndData() {
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java)

View file

@ -23,6 +23,7 @@ import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.seismic.ShakeDetector
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@ -62,8 +63,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY" const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY" const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
@ -149,12 +148,16 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
// analytics // analytics
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
// Rageshake
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
// other // other
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@ -247,8 +250,12 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
} }
fun developerMode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false)
}
fun shouldShowHiddenEvents(): Boolean { fun shouldShowHiddenEvents(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
} }
fun swipeToReplyIsEnabled(): Boolean { fun swipeToReplyIsEnabled(): Boolean {
@ -256,7 +263,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
fun labAllowedExtendedLogging(): Boolean { fun labAllowedExtendedLogging(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
} }
/** /**
@ -730,14 +737,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
/** /**
* Update the rage shake status. * Get the rage shake sensitivity.
*
* @param isEnabled true to enable the rage shake
*/ */
fun setUseRageshake(isEnabled: Boolean) { fun getRageshakeSensitivity(): Int {
defaultPrefs.edit { return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM)
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
}
} }
/** /**

View file

@ -54,8 +54,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
if (isFirstCreation()) { if (isFirstCreation()) {
// display the fragment // display the fragment
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
} }
}
supportFragmentManager.addOnBackStackChangedListener(this) supportFragmentManager.addOnBackStackChangedListener(this)
} }
@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
} }
companion object { companion object {
fun getIntent(context: Context) = Intent(context, VectorSettingsActivity::class.java) fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
const val EXTRA_DIRECT_ACCESS_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
} }

View file

@ -0,0 +1,78 @@
/*
* 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.riotx.features.settings
import androidx.preference.Preference
import androidx.preference.SeekBarPreference
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.preference.VectorSwitchPreference
import im.vector.riotx.features.rageshake.RageShake
class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_advanced_settings
override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
private var rageshake: RageShake? = null
override fun onResume() {
super.onResume()
rageshake = (activity as? VectorBaseActivity)?.rageShake
rageshake?.interceptor = {
(activity as? VectorBaseActivity)?.showSnackbar(getString(R.string.rageshake_detected))
}
}
override fun onPause() {
super.onPause()
rageshake?.interceptor = null
rageshake = null
}
override fun bindPref() {
val isRageShakeAvailable = RageShake.isAvailable(requireContext())
if (isRageShakeAvailable) {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue as? Boolean == true) {
rageshake?.start()
} else {
rageshake?.stop()
}
true
}
findPreference<SeekBarPreference>(VectorPreferences.SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
(activity as? VectorBaseActivity)?.let {
val newValueAsInt = newValue as? Int ?: return@OnPreferenceChangeListener true
rageshake?.setSensitivity(newValueAsInt)
}
true
}
} else {
findPreference<VectorSwitchPreference>("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
}
}
}

View file

@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface
import android.view.KeyEvent
import android.widget.Button import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -33,30 +29,19 @@ import androidx.preference.SwitchPreference
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.ProgressBarPreference
import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.preference.VectorPreferenceDivider
import im.vector.riotx.core.utils.* import im.vector.riotx.core.utils.*
import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysExporter
import im.vector.riotx.features.crypto.keys.KeysImporter import im.vector.riotx.features.crypto.keys.KeysImporter
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor( class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@ -66,9 +51,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy
// used to avoid requesting to enter the password for each deletion
private var mAccountPassword: String = ""
// devices: device IDs and device names // devices: device IDs and device names
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf() private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val mCryptographyCategory by lazy { private val mCryptographyCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
} }
private val mCryptographyCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!!
}
// cryptography manage // cryptography manage
private val mCryptographyManageCategory by lazy { private val mCryptographyManageCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
} }
private val mCryptographyManageCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!!
}
// displayed pushers // displayed pushers
private val mPushersSettingsDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!!
}
private val mPushersSettingsCategory by lazy { private val mPushersSettingsCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
} }
private val mDevicesListSettingsCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
}
private val mDevicesListSettingsCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
}
private val cryptoInfoDeviceNamePreference by lazy { private val cryptoInfoDeviceNamePreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
} }
@ -129,13 +96,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!! findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
} }
override fun onResume() {
super.onResume()
// My device name may have been updated
refreshMyDevice()
}
override fun bindPref() { override fun bindPref() {
// Push target // Push target
refreshPushersList() refreshPushersList()
// Device list
refreshDevicesList()
// Refresh Key Management section // Refresh Key Management section
refreshKeysManagementSection() refreshKeysManagementSection()
@ -151,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
true true
} }
} }
// Rageshake Management
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!.let {
it.isChecked = vectorPreferences.useRageshake()
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
vectorPreferences.setUseRageshake(newValue as Boolean)
true
}
}
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -353,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private fun removeCryptographyPreference() { private fun removeCryptographyPreference() {
preferenceScreen.let { preferenceScreen.let {
it.removePreference(mCryptographyCategory) it.removePreference(mCryptographyCategory)
it.removePreference(mCryptographyCategoryDivider)
// Also remove keys management section // Also remove keys management section
it.removePreference(mCryptographyManageCategory) it.removePreference(mCryptographyManageCategory)
it.removePreference(mCryptographyManageCategoryDivider)
} }
} }
@ -375,7 +333,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceRenameDialog(aMyDeviceInfo) // TODO device can be rename only from the device list screen for the moment
// displayDeviceRenameDialog(aMyDeviceInfo)
true true
} }
@ -428,340 +387,20 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// devices list // devices list
// ============================================================================================================== // ==============================================================================================================
private fun removeDevicesPreference() { private fun refreshMyDevice() {
preferenceScreen.let { // TODO Move to a ViewModel...
it.removePreference(mDevicesListSettingsCategory) session.sessionParams.credentials.deviceId?.let {
it.removePreference(mDevicesListSettingsCategoryDivider) session.getDeviceInfo(it, object : MatrixCallback<DeviceInfo> {
}
}
/**
* Force the refresh of the devices list.<br></br>
* The devices list is the list of the devices where the user as looged in.
* It can be any mobile device, as any browser.
*/
private fun refreshDevicesList() {
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
// display a spinner while loading the devices list
if (0 == mDevicesListSettingsCategory.preferenceCount) {
activity?.let {
val preference = ProgressBarPreference(it)
mDevicesListSettingsCategory.addPreference(preference)
}
}
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
if (!isAdded) {
return
}
if (data.devices?.isEmpty() == true) {
removeDevicesPreference()
} else {
buildDevicesSettings(data.devices!!)
}
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (!isAdded) { // Ignore for this time?...
return
} }
removeDevicesPreference() override fun onSuccess(data: DeviceInfo) {
onCommonDone(failure.message) mMyDeviceInfo = data
} refreshCryptographyPreference(data)
})
} else {
removeDevicesPreference()
removeCryptographyPreference()
}
}
/**
* Build the devices portion of the settings.<br></br>
* Each row correspond to a device ID and its corresponding device name. Clicking on the row
* display a dialog containing: the device ID, the device name and the "last seen" information.
*
* @param aDeviceInfoList the list of the devices
*/
private fun buildDevicesSettings(aDeviceInfoList: List<DeviceInfo>) {
var preference: VectorPreference
var typeFaceHighlight: Int
var isNewList = true
val myDeviceId = session.sessionParams.credentials.deviceId
if (aDeviceInfoList.size == mDevicesNameList.size) {
isNewList = !mDevicesNameList.containsAll(aDeviceInfoList)
}
if (isNewList) {
var prefIndex = 0
mDevicesNameList.clear()
mDevicesNameList.addAll(aDeviceInfoList)
// sort before display: most recent first
mDevicesNameList.sortByLastSeen()
// start from scratch: remove the displayed ones
mDevicesListSettingsCategory.removeAll()
for (deviceInfo in mDevicesNameList) {
// set bold to distinguish current device ID
if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) {
mMyDeviceInfo = deviceInfo
typeFaceHighlight = Typeface.BOLD
} else {
typeFaceHighlight = Typeface.NORMAL
}
// add the edit text preference
preference = VectorPreference(requireActivity()).apply {
mTypeface = typeFaceHighlight
}
if (null == deviceInfo.deviceId && null == deviceInfo.displayName) {
continue
} else {
if (null != deviceInfo.deviceId) {
preference.title = deviceInfo.deviceId
}
// display name parameter can be null (new JSON API)
if (null != deviceInfo.displayName) {
preference.summary = deviceInfo.displayName
}
}
preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex
prefIndex++
// onClick handler: display device details dialog
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceDetailsDialog(deviceInfo)
true
}
mDevicesListSettingsCategory.addPreference(preference)
}
refreshCryptographyPreference(mMyDeviceInfo)
}
}
/**
* Display a dialog containing the device ID, the device name and the "last seen" information.<>
* This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog])
*
* @param aDeviceInfo the device information
*/
private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) {
activity?.let {
val builder = AlertDialog.Builder(it)
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_details, null)
var textView = layout.findViewById<TextView>(R.id.device_id)
textView.text = aDeviceInfo.deviceId
// device name
textView = layout.findViewById(R.id.device_name)
val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName
textView.text = displayName
// last seen info
textView = layout.findViewById(R.id.device_last_seen)
val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
textView.text = lastSeenInfo
// title & icon
builder.setTitle(R.string.devices_details_dialog_title)
.setIcon(android.R.drawable.ic_dialog_info)
.setView(layout)
.setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) }
// disable the deletion for our own device
if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) {
builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) }
}
builder.setNeutralButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.show()
}
}
/**
* Display an alert dialog to rename a device
*
* @param aDeviceInfoToRename device info
*/
private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) {
activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
input.setText(aDeviceInfoToRename.displayName)
AlertDialog.Builder(it)
.setTitle(R.string.devices_details_device_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
displayLoadingView()
val newName = input.text.toString()
session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// search which preference is updated
val count = mDevicesListSettingsCategory.preferenceCount
for (i in 0 until count) {
val pref = mDevicesListSettingsCategory.getPreference(i)
if (aDeviceInfoToRename.deviceId == pref.title) {
pref.summary = newName
}
}
// detect if the updated device is the current account one
if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) {
cryptoInfoDeviceNamePreference.summary = newName
}
// Also change the display name in aDeviceInfoToRename, in case of multiple renaming
aDeviceInfoToRename.displayName = newName
}
override fun onFailure(failure: Throwable) {
onCommonDone(failure.localizedMessage)
} }
}) })
} }
.setNegativeButton(R.string.cancel, null)
.show()
}
}
/**
* Try to delete a device.
*
* @param deviceInfo the device to delete
*/
private fun deleteDevice(deviceInfo: DeviceInfo) {
val deviceId = deviceInfo.deviceId
if (deviceId == null) {
Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
return
}
displayLoadingView()
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
var isPasswordRequestFound = false
if (failure is Failure.RegistrationFlowError) {
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session)
}
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
onCommonDone(failure.localizedMessage)
}
}
})
}
/**
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) {
if (mAccountPassword.isNotEmpty()) {
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
} else {
activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(it)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (passwordEditText.toString().isEmpty()) {
it.toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
})
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
}
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
hideLoadingView()
return@OnKeyListener true
}
false
})
.show()
}
}
}
private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) {
session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
// Password is maybe not good
onCommonDone(failure.localizedMessage)
mAccountPassword = ""
}
})
} }
// ============================================================================================================== // ==============================================================================================================
@ -860,6 +499,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
// TODO i18n // TODO i18n
private const val LABEL_UNAVAILABLE_DATA = "none" const val LABEL_UNAVAILABLE_DATA = "none"
} }
} }

View file

@ -0,0 +1,109 @@
/*
* 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.riotx.features.settings.devices
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
/**
* A list item for Device.
*/
@EpoxyModelClass(layout = R.layout.item_device)
abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
@EpoxyAttribute
lateinit var deviceInfo: DeviceInfo
@EpoxyAttribute
var currentDevice = false
@EpoxyAttribute
var buttonsVisible = false
@EpoxyAttribute
var itemClickAction: (() -> Unit)? = null
@EpoxyAttribute
var renameClickAction: (() -> Unit)? = null
@EpoxyAttribute
var deleteClickAction: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() }
holder.displayNameText.text = deviceInfo.displayName ?: ""
holder.deviceIdText.text = deviceInfo.deviceId ?: ""
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
listOf(
holder.displayNameLabelText,
holder.displayNameText,
holder.deviceIdLabelText,
holder.deviceIdText,
holder.deviceLastSeenLabelText,
holder.deviceLastSeenText
).map {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
}
holder.buttonDelete.isVisible = !currentDevice
holder.buttons.isVisible = buttonsVisible
holder.buttonRename.setOnClickListener { renameClickAction?.invoke() }
holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() }
}
class Holder : VectorEpoxyHolder() {
val root by bind<ViewGroup>(R.id.itemDeviceRoot)
val displayNameLabelText by bind<TextView>(R.id.itemDeviceDisplayNameLabel)
val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName)
val deviceIdLabelText by bind<TextView>(R.id.itemDeviceIdLabel)
val deviceIdText by bind<TextView>(R.id.itemDeviceId)
val deviceLastSeenLabelText by bind<TextView>(R.id.itemDeviceLastSeenLabel)
val deviceLastSeenText by bind<TextView>(R.id.itemDeviceLastSeen)
val buttons by bind<View>(R.id.itemDeviceButtons)
val buttonDelete by bind<View>(R.id.itemDeviceDelete)
val buttonRename by bind<View>(R.id.itemDeviceRename)
}
}

View file

@ -0,0 +1,129 @@
/*
* 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.riotx.features.settings.devices
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItemHeader
import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider) : EpoxyController() {
var callback: Callback? = null
private var viewState: DevicesViewState? = null
init {
requestModelBuild()
}
fun update(viewState: DevicesViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
buildDevicesModels(nonNullViewState)
}
private fun buildDevicesModels(state: DevicesViewState) {
when (val devices = state.devices) {
is Loading,
is Uninitialized ->
loadingItem {
id("loading")
}
is Fail ->
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(devices.error))
listener { callback?.retry() }
}
is Success ->
buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId)
}
}
private fun buildDevicesList(devices: List<DeviceInfo>, myDeviceId: String, currentExpandedDeviceId: String?) {
// Current device
genericItemHeader {
id("current")
text(stringProvider.getString(R.string.devices_current_device))
}
devices
.filter {
it.deviceId == myDeviceId
}
.forEachIndexed { idx, deviceInfo ->
deviceItem {
id("myDevice$idx")
deviceInfo(deviceInfo)
currentDevice(true)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) }
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
}
}
// Other devices
if (devices.size > 1) {
genericItemHeader {
id("others")
text(stringProvider.getString(R.string.devices_other_devices))
}
devices
.filter {
it.deviceId != myDeviceId
}
// sort before display: most recent first
.sortByLastSeen()
.forEachIndexed { idx, deviceInfo ->
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem {
id("device$idx")
deviceInfo(deviceInfo)
currentDevice(isCurrentDevice)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) }
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
}
}
}
}
interface Callback {
fun retry()
fun onDeviceClicked(deviceInfo: DeviceInfo)
fun onRenameDevice(deviceInfo: DeviceInfo)
fun onDeleteDevice(deviceInfo: DeviceInfo)
}
}

View file

@ -0,0 +1,268 @@
/*
* 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.riotx.features.settings.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
data class DevicesViewState(
val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized,
val currentExpandedDeviceId: String? = null,
val request: Async<Unit> = Uninitialized
) : MvRxState
sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction()
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
}
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
private val session: Session)
: VectorViewModel<DevicesViewState, DevicesAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DevicesViewState): DevicesViewModel
}
companion object : MvRxViewModelFactory<DevicesViewModel, DevicesViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? {
val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.devicesViewModelFactory.create(state)
}
}
// temp storage when we ask for the user password
private var _currentDeviceId: String? = null
private var _currentSession: String? = null
private val _requestPasswordLiveData = MutableLiveData<LiveEvent<Unit>>()
val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
get() = _requestPasswordLiveData
init {
refreshDevicesList()
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
private fun refreshDevicesList() {
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
setState {
copy(
devices = Loading()
)
}
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
devices = Success(data.devices.orEmpty())
)
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(
devices = Fail(failure)
)
}
}
})
} else {
// Should not happen
}
}
override fun handle(action: DevicesAction) {
return when (action) {
is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action)
is DevicesAction.ToggleDevice -> handleToggleDevice(action)
}
}
private fun handleToggleDevice(action: DevicesAction.ToggleDevice) {
withState {
setState {
copy(
currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId
)
}
}
}
private fun handleRename(action: DevicesAction.Rename) {
session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
})
}
/**
* Try to delete a device.
*/
private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceInfo.deviceId
if (deviceId == null) {
Timber.e("## handleDelete(): sanity check failure")
return
}
setState {
copy(
request = Loading()
)
}
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
var isPasswordRequestFound = false
if (failure is Failure.RegistrationFlowError) {
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
_currentDeviceId = deviceId
_currentSession = failure.registrationFlowResponse.session
setState {
copy(
request = Success(Unit)
)
}
_requestPasswordLiveData.postLiveEvent(Unit)
}
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
}
override fun onSuccess(data: Unit) {
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
})
}
private fun handlePassword(action: DevicesAction.Password) {
val currentDeviceId = _currentDeviceId
if (currentDeviceId.isNullOrBlank()) {
// Abort
return
}
setState {
copy(
request = Loading()
)
}
session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_currentDeviceId = null
_currentSession = null
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
_currentDeviceId = null
_currentSession = null
// Password is maybe not good
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
})
}
}

View file

@ -0,0 +1,181 @@
/*
* 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.riotx.features.settings.devices
import android.content.DialogInterface
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject
/**
* Display the list of the user's device
*/
class VectorSettingsDevicesFragment @Inject constructor(
val devicesViewModelFactory: DevicesViewModel.Factory,
private val devicesController: DevicesController
) : VectorBaseFragment(), DevicesController.Callback {
// used to avoid requesting to enter the password for each deletion
private var mAccountPassword: String = ""
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val devicesViewModel: DevicesViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
devicesController.callback = this
recyclerView.configureWith(devicesController, showDivider = true)
devicesViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it)
// Password is maybe not good, for safety measure, reset it here
mAccountPassword = ""
}
devicesViewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog()
}
}
override fun onDestroyView() {
devicesController.callback = null
recyclerView.cleanup()
super.onDestroyView()
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list)
}
private fun displayErrorDialog(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
}
override fun onDeleteDevice(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
}
override fun onRenameDevice(deviceInfo: DeviceInfo) {
displayDeviceRenameDialog(deviceInfo)
}
override fun retry() {
devicesViewModel.handle(DevicesAction.Retry)
}
/**
* Display an alert dialog to rename a device
*
* @param deviceInfo device info
*/
private fun displayDeviceRenameDialog(deviceInfo: DeviceInfo) {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
input.setText(deviceInfo.displayName)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.devices_details_device_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString()
devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
/**
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog() {
if (mAccountPassword.isNotEmpty()) {
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
} else {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(requireActivity())
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (passwordEditText.toString().isEmpty()) {
requireActivity().toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
})
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.show()
}
}
override fun invalidate() = withState(devicesViewModel) { state ->
devicesController.update(state)
handleRequestStatus(state.request)
}
private fun handleRequestStatus(unIgnoreRequest: Async<Unit>) {
when (unIgnoreRequest) {
is Loading -> waiting_view.isVisible = true
else -> waiting_view.isVisible = false
}
}
}

View file

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/device_container_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:id="@+id/device_id_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device id" />
<TextView
android:id="@+id/device_name_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device name" />
<TextView
android:id="@+id/device_last_seen_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_last_seen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/itemAutocompleteEmoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="@color/black"
android:textSize="20dp"
tools:ignore="SpUsage"
tools:text="@sample/reactions.json/data/reaction" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/itemAutocompleteEmojiName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
android:textStyle="bold"
tools:text="name" />
<TextView
android:id="@+id/itemAutocompleteEmojiSubname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
android:visibility="gone"
tools:text="name"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="8dp"
android:text="@string/autocomplete_limited_results"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemDeviceRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/itemDeviceDisplayNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceDisplayName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="Android phone" />
<TextView
android:id="@+id/itemDeviceIdLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="XUIDERFZAA" />
<TextView
android:id="@+id/itemDeviceLastSeenLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceLastSeen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
<LinearLayout
android:id="@+id/itemDeviceButtons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceRename"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rename" />
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceDelete"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/delete"
android:textColor="@color/riotx_notice"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="?vctr_shadow_bottom" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="?vctr_preference_divider_color" />
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="?vctr_shadow_top" />
</LinearLayout>

View file

@ -63,9 +63,6 @@
<!-- room notification text color (typing, unsent...) --> <!-- room notification text color (typing, unsent...) -->
<attr name="vctr_room_notification_text_color" format="color" /> <attr name="vctr_room_notification_text_color" format="color" />
<!-- color for dividers in settings -->
<attr name="vctr_preference_divider_color" format="color" />
<!-- icon colors --> <!-- icon colors -->
<attr name="vctr_icon_tint_on_light_action_bar_color" format="color" /> <attr name="vctr_icon_tint_on_light_action_bar_color" format="color" />
<attr name="vctr_icon_tint_on_dark_action_bar_color" format="color" /> <attr name="vctr_icon_tint_on_dark_action_bar_color" format="color" />

View file

@ -5,4 +5,19 @@
<string name="notification_initial_sync">Initial Sync…</string> <string name="notification_initial_sync">Initial Sync…</string>
<string name="settings_show_devices_list">See all my devices</string>
<string name="settings_advanced_settings">Advanced settings</string>
<string name="settings_developer_mode">Developer mode</string>
<string name="settings_developer_mode_summary">The developer mode activates hidden features and may also make the application less stable. For developers only!</string>
<string name="settings_rageshake">Rageshake</string>
<string name="settings_rageshake_detection_threshold">Detection threshold</string>
<string name="settings_rageshake_detection_threshold_summary">Shake your phone to test the detection threshold</string>
<string name="rageshake_detected">Shake detected!</string>
<string name="settings">Settings</string>
<string name="devices_current_device">Current device</string>
<string name="devices_other_devices">Other devices</string>
<string name="autocomplete_limited_results">Showing only the first results, type more letters…</string>
</resources> </resources>

View file

@ -72,9 +72,6 @@
<item name="vctr_tab_home_secondary">@color/primary_color_dark_black</item> <item name="vctr_tab_home_secondary">@color/primary_color_dark_black</item>
<item name="vctr_list_divider_color">@color/list_divider_color_black</item> <item name="vctr_list_divider_color">@color/list_divider_color_black</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_black</item>
<item name="vctr_markdown_block_background_color">#FF4D4D4D</item> <item name="vctr_markdown_block_background_color">#FF4D4D4D</item>
<item name="vctr_pill_receipt">@drawable/pill_receipt_black</item> <item name="vctr_pill_receipt">@drawable/pill_receipt_black</item>

View file

@ -139,9 +139,6 @@
<!--Notice (secondary)--> <!--Notice (secondary)-->
<item name="vctr_room_notification_text_color">#FF61708b</item> <item name="vctr_room_notification_text_color">#FF61708b</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_dark</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@android:color/white</item> <item name="vctr_settings_icon_tint_color">@android:color/white</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -139,9 +139,6 @@
<!--Notice (secondary)--> <!--Notice (secondary)-->
<item name="vctr_room_notification_text_color">#FF61708b</item> <item name="vctr_room_notification_text_color">#FF61708b</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_light</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@android:color/black</item> <item name="vctr_settings_icon_tint_color">@android:color/black</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -88,9 +88,6 @@
<!-- room notification text color (typing, unsent...) --> <!-- room notification text color (typing, unsent...) -->
<item name="vctr_room_notification_text_color">#a0a29f</item> <item name="vctr_room_notification_text_color">#a0a29f</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">#e1e1e1</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@color/accent_color_status</item> <item name="vctr_settings_icon_tint_color">@color/accent_color_status</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_developer_mode">
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:summary="@string/settings_developer_mode_summary"
android:title="@string/settings_developer_mode" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
android:summary="@string/labs_allow_extended_logging_summary"
android:title="@string/labs_allow_extended_logging" />
<!-- TODO Display unsupported events -->
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_RAGE_SHAKE_CATEGORY_KEY"
android:title="@string/settings_rageshake">
<im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_USE_RAGE_SHAKE_KEY"
android:title="@string/send_bug_report_rage_shake" />
<androidx.preference.SeekBarPreference
android:defaultValue="13"
android:dependency="SETTINGS_USE_RAGE_SHAKE_KEY"
android:key="SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
android:max="15"
android:summary="@string/settings_rageshake_detection_threshold_summary"
android:title="@string/settings_rageshake_detection_threshold"
app:min="11" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_notifications">
<im.vector.riotx.core.preference.VectorPreference
android:persistent="false"
android:title="@string/settings_notifications_targets"
app:fragment="im.vector.riotx.features.settings.push.PushGatewaysFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:persistent="false"
android:title="@string/settings_push_rules"
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen>

View file

@ -45,11 +45,10 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CONTACT_PREFERENCE_KEYS" android:key="SETTINGS_CONTACT_PREFERENCE_KEYS"
android:title="@string/settings_contact"> android:title="@string/settings_contact"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="CONTACT_BOOK_ACCESS_KEY" android:key="CONTACT_BOOK_ACCESS_KEY"
@ -62,8 +61,6 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_advanced"> <im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_advanced">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
@ -74,11 +71,12 @@
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_HOME_SERVER_PREFERENCE_KEY" android:key="SETTINGS_HOME_SERVER_PREFERENCE_KEY"
android:title="@string/settings_home_server" android:title="@string/settings_home_server"
tools:summary="@string/default_hs_server_url" /> tools:summary="https://homeserver.org" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
android:title="@string/settings_identity_server" android:title="@string/settings_identity_server"
app:isPreferenceVisible="@bool/false_not_implemented"
tools:summary="https://identity.server.url" /> tools:summary="https://identity.server.url" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
@ -91,10 +89,7 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider /> <im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/action_sign_out">
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:title="@string/action_sign_out">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SIGN_OUT_KEY" android:key="SETTINGS_SIGN_OUT_KEY"
@ -104,7 +99,8 @@
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY" android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY"
android:title="@string/settings_deactivate_account_section"> android:title="@string/settings_deactivate_account_section"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY" android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"

View file

@ -2,10 +2,6 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_OTHERS_PREFERENCE_KEY"
android:title="@string/settings_other">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="APP_INFO_LINK_PREFERENCE_KEY" android:key="APP_INFO_LINK_PREFERENCE_KEY"
android:summary="@string/settings_app_info_link_summary" android:summary="@string/settings_app_info_link_summary"
@ -46,6 +42,4 @@
android:key="SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY" android:key="SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_other_third_party_notices" /> android:title="@string/settings_other_third_party_notices" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -34,24 +34,12 @@
<!--android:summary="@string/settings_labs_enable_send_voice_summary"--> <!--android:summary="@string/settings_labs_enable_send_voice_summary"-->
<!--android:title="@string/settings_labs_enable_send_voice" />--> <!--android:title="@string/settings_labs_enable_send_voice" />-->
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
android:title="@string/labs_swipe_to_reply_in_timeline" /> android:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
android:summary="@string/labs_allow_extended_logging_summary"
android:title="@string/labs_allow_extended_logging" />
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>--> <!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -36,8 +36,6 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<!-- For API < 26 --> <!-- For API < 26 -->
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/settings_notification_ringtone" android:dialogTitle="@string/settings_notification_ringtone"

View file

@ -67,27 +67,8 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /> android:title="@string/settings_notifications_targets" /-->
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" /-->
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_expert">
<im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:persistent="false"
android:title="@string/settings_notifications_targets"
app:fragment="im.vector.riotx.features.settings.push.PushGatewaysFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:persistent="false"
android:title="@string/settings_push_rules"
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -30,13 +30,15 @@
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
android:summary="@string/settings_inline_url_preview_summary" android:summary="@string/settings_inline_url_preview_summary"
android:title="@string/settings_inline_url_preview" /> android:title="@string/settings_inline_url_preview"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY" android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
android:summary="@string/settings_send_typing_notifs_summary" android:summary="@string/settings_send_typing_notifs_summary"
android:title="@string/settings_send_typing_notifs" /> android:title="@string/settings_send_typing_notifs"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
@ -46,38 +48,45 @@
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
android:title="@string/settings_always_show_timestamps" /> android:title="@string/settings_always_show_timestamps"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_12_24_TIMESTAMPS_KEY" android:key="SETTINGS_12_24_TIMESTAMPS_KEY"
android:title="@string/settings_12_24_timestamps" /> android:title="@string/settings_12_24_timestamps"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_READ_RECEIPTS_KEY" android:key="SETTINGS_SHOW_READ_RECEIPTS_KEY"
android:summary="@string/settings_show_read_receipts_summary" android:summary="@string/settings_show_read_receipts_summary"
android:title="@string/settings_show_read_receipts" /> android:title="@string/settings_show_read_receipts"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"
android:summary="@string/settings_show_join_leave_messages_summary" android:summary="@string/settings_show_join_leave_messages_summary"
android:title="@string/settings_show_join_leave_messages" /> android:title="@string/settings_show_join_leave_messages"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY" android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"
android:summary="@string/settings_show_avatar_display_name_changes_messages_summary" android:summary="@string/settings_show_avatar_display_name_changes_messages_summary"
android:title="@string/settings_show_avatar_display_name_changes_messages" /> android:title="@string/settings_show_avatar_display_name_changes_messages"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_VIBRATE_ON_MENTION_KEY" android:key="SETTINGS_VIBRATE_ON_MENTION_KEY"
android:title="@string/settings_vibrate_on_mention" /> android:title="@string/settings_vibrate_on_mention"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_SEND_MESSAGE_WITH_ENTER" android:key="SETTINGS_SEND_MESSAGE_WITH_ENTER"
android:summary="@string/settings_send_message_with_enter_summary" android:summary="@string/settings_send_message_with_enter_summary"
android:title="@string/settings_send_message_with_enter" /> android:title="@string/settings_send_message_with_enter"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorListPreference <im.vector.riotx.core.preference.VectorListPreference
android:defaultValue="always" android:defaultValue="always"
@ -85,15 +94,15 @@
android:entryValues="@array/show_info_area_values" android:entryValues="@array/show_info_area_values"
android:key="SETTINGS_SHOW_INFO_AREA_KEY" android:key="SETTINGS_SHOW_INFO_AREA_KEY"
android:summary="%s" android:summary="%s"
android:title="@string/settings_info_area_show" /> android:title="@string/settings_info_area_show"
app:isPreferenceVisible="@bool/false_not_implemented" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_HOME_DISPLAY_KEY" android:key="SETTINGS_HOME_DISPLAY_KEY"
android:title="@string/settings_home_display"> android:title="@string/settings_home_display"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
@ -107,9 +116,9 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider /> <im.vector.riotx.core.preference.VectorPreferenceCategory
android:title="@string/settings_media"
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_media"> app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_MEDIA_SAVING_PERIOD_KEY" android:key="SETTINGS_MEDIA_SAVING_PERIOD_KEY"

View file

@ -3,58 +3,54 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_general" android:icon="@drawable/ic_settings_root_general"
android:title="@string/settings_general_title" android:title="@string/settings_general_title"
app:fragment="im.vector.riotx.features.settings.VectorSettingsGeneralFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsGeneralFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:enabled="@bool/false_not_implemented" android:enabled="@bool/false_not_implemented"
android:icon="@drawable/ic_settings_root_flair" android:icon="@drawable/ic_settings_root_flair"
android:title="@string/settings_flair" android:title="@string/settings_flair"
app:fragment="im.vector.riotx.features.settings.VectorSettingsFlairFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsFlairFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_notification" android:icon="@drawable/ic_settings_root_notification"
android:key="SETTINGS_NOTIFICATIONS_KEY" android:key="SETTINGS_NOTIFICATIONS_KEY"
android:title="@string/settings_notifications" android:title="@string/settings_notifications"
app:fragment="im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_preferences" android:icon="@drawable/ic_settings_root_preferences"
android:title="@string/settings_preferences" android:title="@string/settings_preferences"
app:fragment="im.vector.riotx.features.settings.VectorSettingsPreferencesFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsPreferencesFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:enabled="@bool/false_not_implemented" android:enabled="@bool/false_not_implemented"
android:icon="@drawable/ic_settings_root_call" android:icon="@drawable/ic_settings_root_call"
android:title="@string/preference_voice_and_video" android:title="@string/preference_voice_and_video"
app:fragment="im.vector.riotx.features.settings.VectorSettingsVoiceVideoFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsVoiceVideoFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_ignored_users" android:icon="@drawable/ic_settings_root_ignored_users"
android:title="@string/settings_ignored_users" android:title="@string/settings_ignored_users"
app:fragment="im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment" /> app:fragment="im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_security_privacy" android:icon="@drawable/ic_settings_root_security_privacy"
android:title="@string/settings_security_and_privacy" android:title="@string/settings_security_and_privacy"
app:fragment="im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_labs" android:icon="@drawable/ic_settings_root_labs"
android:title="@string/room_settings_labs_pref_title" android:title="@string/room_settings_labs_pref_title"
app:fragment="im.vector.riotx.features.settings.VectorSettingsLabsFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsLabsFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent" android:icon="@drawable/ic_settings_root_general"
android:title="@string/settings_advanced_settings"
app:fragment="im.vector.riotx.features.settings.VectorSettingsAdvancedSettingsFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:icon="@drawable/ic_settings_root_help_about" android:icon="@drawable/ic_settings_root_help_about"
android:title="@string/preference_root_help_about" android:title="@string/preference_root_help_about"
app:fragment="im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment" />

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- ************ Cryptography section ************ --> <!-- ************ Cryptography section ************ -->
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
@ -21,11 +22,22 @@
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary" android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title" /> android:title="@string/encryption_never_send_to_unverified_devices_title"
app:isPreferenceVisible="@bool/false_not_implemented" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" /> <!-- devices list entry point -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_devices_list">
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_show_devices_list"
app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
@ -48,18 +60,10 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" />
<!-- devices list: device ids + device names -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_devices_list" />
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY" android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
android:title="@string/settings_analytics"> android:title="@string/settings_analytics"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
@ -67,9 +71,6 @@
android:summary="@string/settings_opt_in_of_analytics_summary" android:summary="@string/settings_opt_in_of_analytics_summary"
android:title="@string/settings_opt_in_of_analytics" /> android:title="@string/settings_opt_in_of_analytics" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_USE_RAGE_SHAKE_KEY"
android:title="@string/send_bug_report_rage_shake" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>