Merge branch 'develop' into feature/bca/home_empty_screens

This commit is contained in:
Benoit Marty 2020-11-27 12:02:30 +01:00 committed by GitHub
commit 32d42794dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 4407 additions and 2576 deletions

View File

@ -39,7 +39,7 @@ We do not forget all translators, for their work of translating Element into man
Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
Name | Matrix ID | GitHub
----------|-----------------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
TR_SLimey | @tr_slimey:an-atom-in.space | [TR-SLimey](https://github.com/TR-SLimey)

View File

@ -2,7 +2,9 @@ Changes in Element 1.0.11 (2020-XX-XX)
===================================================
Features ✨:
-
- Create DMs with users by scanning their QR code (#2025)
- Add Invite friends quick invite actions (#2348)
- Add friend by scanning QR code, show your code to friends (#2025)
Improvements 🙌:
- New room creation tile with quick action (#2346)
@ -13,6 +15,7 @@ Improvements 🙌:
- Room creation form: add advanced section to disable federation (#1314)
- Move "Enable Encryption" from room setting screen to room profile screen (#2394)
- Home empty screens quick design update (#2347)
- Improve Invite user screen (seamless search for matrix ID)
Bugfix 🐛:
- Fix crash on AttachmentViewer (#2365)
@ -24,6 +27,8 @@ Bugfix 🐛:
- Try to fix cropped image in timeline (#2126)
- Registration: annoying error message scares every new user when they add an email (#2391)
- Fix jitsi integration for those with non-vanilla dialler frameworks
- Update profile has no effect if user is in zero rooms
- Fix issues with matrix.to deep linking (#2349)
Translations 🗣:
-

View File

@ -0,0 +1 @@
// TODO

View File

@ -0,0 +1,30 @@
Element és un nou tipus d'aplicació de missatgeria i col·laboració que:
1. Et dóna a tu el control per preservar la teva privadesa
2. Et permet comunicar-te amb qualsevol persona de la xarxa Matrix i, fins i tot més enllà gràcies a integracions amb altres aplicacions com Slack
3. Et protegeix de la publicitat, l'obtenció no desitjada de dades i dels navegadors amb accés controlat
4. T'assegura a tu mitjançant l'encriptació d'extrem a extrem i amb signatures creuades per verificar els altres
Element és completament diferent a les altres aplicacions de missatgeria i col·laboració ja que és descentralitzat i de codi obert.
Element et deixa triar l'allotjament perquè disposis de privadesa, propietat i control de les teves dades i converses. Et dóna accés a una xarxa oberta perquè no et quedis únicament parlant amb els usuaris d'Element.
Element pot fer tot això ja que opera sobre Matrix - l'estàndard per a les comunicacions obertes i descentralitzades.
Element et dóna el control perquè et deixa escollir qui vols que allotgi les teves converses. Des de l'aplicació d'Element, pots triar l'allotjament de diferents maneres:
1. Crea un compte gratuït al servidor públic de matrix.org allotjat pels desenvolupadors de Matrix o tria'n un entre els milers de servidors públics creats per voluntaris
2. Allotja tu mateix el teu compte en el teu propi servidor
3. Registra el compte en un servidor personalitzat subscrivint-te a la plataforma d'Element Matrix Services (EMS)
<b>Per què escollir Element?</b>
<b>PROPIETAT DE LES TEVES DADES</b>: Tu decideixes a on desar les teves dades i missatges. Tu les controles i n'ets el propietari, no una mega-corporació que s'aprofita de les teves dades o les cedeix a tercers.
<b>MISSATGERIA I COL·LABORACIÓ OBERTA</b>: Pots parlar amb qualsevol que estigui a la xarxa Matrix, ja sigui amb Element o amb qualsevol altre aplicació Matrix, fins i tot encara que utilitzin sistemes de missatgeria diferents com Slack, IRC o XMPP.
<b>SUPER-SEGUR</b>: Encriptació d'extrem a extrem real (només qui està conversant pot desxifrar els missatges), i amb signatures creuades per a verificar els dispositius dels participants en les converses.
<b>COMUNICACIÓ COMPLETA</b>: Missatgeria, veu i video-trucades, compartició de fitxers, compartició de pantalla i un munt d'integracions, bots i ginys. Crea sales, comunitats, mantén-te en contacte i enllesteix el que et proposes.
<b>A TOT ARREU</b>: Mantingues el contacte des de qualsevol lloc on siguis, amb un historial de missatges totalment sincronitzat entre tots els teus dispositius i també a la web: https://app.element.io.

View File

@ -0,0 +1 @@
Xat i VoIP segurs i descentralitzats. Protegeix les teves dades de tercers.

View File

@ -0,0 +1 @@
Element (anteriorment Riot.im)

View File

@ -0,0 +1 @@
// TODO

View File

@ -0,0 +1 @@
// TODO

View File

@ -1,30 +1,30 @@
Element es un nuevo tipo de aplicación de mensajería y colaboración que:
1. Le da el control para preservar su privacidad
2. Le permite comunicarse con cualquier persona en la red Matrix e incluso más allá al integrarse con aplicaciones como Slack.
3. Te protege de la publicidad, la minería de datos y los jardines vallados.
4. Lo protege a través del cifrado de un extremo a otro, con firma cruzada para verificar a otros
1. Te da el control para preservar su privacidad
2. Te permite comunicarse con cualquier persona en la red Matrix e incluso más allá al integrarse con aplicaciones como Slack
3. Te protege de la publicidad, la minería de datos y los jardines vallados
4. Te protege a través de encriptación de Extremo-a-Extremo, con firma cruzada para verificar a otros
Element es completamente diferente de otras aplicaciones de mensajería y colaboración porque es descentralizado y de código abierto.
Element le permite autohospedarse, o elegir un host, para que tenga privacidad, propiedad y control de sus datos y conversaciones. Te da acceso a una red abierta; para que no se quede atascado hablando solo con otros usuarios de Element. Y es muy seguro.
Element te permite tener su propio servidor privado, o elegir uno público, para que tenga privacidad, posesión, y control de sus datos y conversaciones. Te da acceso a una red abierta; para que no se quede atrapado hablando solo con otros usuarios de Element. Y es muy seguro.
Element puede hacer todo esto porque opera en Matrix, el estándar para la comunicación abierta y descentralizada.
Element te da el control permitiéndote elegir quién aloja tus conversaciones. Desde la aplicación Element, puede elegir hospedar de diferentes maneras:
Element te da el control permitiéndote elegir quién aloja tus conversaciones. Desde la aplicación Element, puedes elegir hospedar de diferentes maneras:
1. Obtenga una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elija entre miles de servidores públicos alojados por voluntarios
2. Autohospede su cuenta ejecutando un servidor en su propio hardware
3. Regístrese para obtener una cuenta en un servidor personalizado simplemente suscribiéndose a la plataforma de alojamiento de Element Matrix Services
1. Obtén una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elije entre miles de servidores públicos alojados por voluntarios
2. Autohospeda tu cuenta con un servidor en tu propio hardware
3. Regístrate para obtener una cuenta en un servidor personalizado simplemente suscribiéndote a la plataforma de alojamiento de Element Matrix Services
<b>¿Por qué elegir Element?</b>
<b>POSEE SUS DATOS</b>: Tú decides dónde guardar tus datos y mensajes. Usted es el propietario y lo controla, no algún MEGACORP que extraiga sus datos o dé acceso a terceros.
<b>TOMA POSESIÓN DE TUS DATOS</b>: Tú decides dónde guardar tus datos y mensajes. Tú eres el propietario y quien lo controla, no alguna MEGACORP que extrae tu datos o da acceso a terceros.
<b>MENSAJERÍA ABIERTA Y COLABORACIÓN</b>: Puede chatear con cualquier otra persona en la red de Matrix, ya sea que estén usando Element u otra aplicación de Matrix, e incluso si están usando un sistema de mensajería diferente como Slack, IRC o XMPP.
<b>MENSAJERÍA ABIERTA Y COLABORACIÓN</b>: Puede chatear con cualquier otra persona en la red de Matrix, tanto si usan Element u otra aplicación de Matrix, e incluso si están usando un sistema de mensajería diferente como Slack, IRC o XMPP.
<b>SUPER SEGURO</b>: Cifrado real de extremo a extremo (solo aquellos en la conversación pueden descifrar mensajes) y firma cruzada para verificar los dispositivos de los participantes de la conversación.
<b>SUPER SEGURO</b>: Encriptación de Extremo-a-Extremo real (solo aquellos en la conversación pueden descifrar mensajes) y firma cruzada para verificar los dispositivos de los participantes de la conversación.
<b>COMUNICACIÓN COMPLETA</b>: Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Construya salas, comunidades, manténgase en contacto y haga las cosas.
<b>COMUNICACIÓN COMPLETA</b>: Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Crea salas, comunidades, mantente en contacto y organízate con eficacia.
<b>EN TODAS PARTES</b>: Manténgase en contacto donde quiera que esté con un historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io.
<b>EN TODAS PARTES</b>: Mantente en contacto donde quiera que estés con un historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io.

View File

@ -1 +1 @@
Chat y VoIP descentralizados seguros. Mantenga sus datos a salvo de terceros.
Chat y VoIP descentralizados y seguros. Mantén tus datos a salvo de terceros.

View File

@ -1 +1 @@
Element (anteriorment Riot.im)
Element (previamente Riot.im)

View File

@ -0,0 +1 @@
// برای انجام

View File

@ -0,0 +1 @@
// DA FARE

View File

@ -0,0 +1 @@
Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter.

View File

@ -0,0 +1 @@
Element (tidligere Riot.im)

View File

@ -0,0 +1 @@
// A FAZER

View File

@ -0,0 +1 @@
// ATT GÖRA

View File

@ -0,0 +1 @@
// 待辦事項

View File

@ -0,0 +1,30 @@
Element 是一種新型態的即時通訊軟體與協作應用程式:
1. 自己的隱私自己掌控
2. 讓您與任何在 Matrix 網路中的人通訊,甚至可與如 Slack 等的應用程式整合
3. 保護您免受廣告、資料採礦與圍牆花園的侵害
4. 透過端到端加密保護您,並使用交叉簽章來驗證其他人
Element 是去中心化且開放原始碼的應用程式,因此與其他即時通訊與協作軟體完全不同。
Element 讓您可以自架(或是自行選擇服務提供者)所以您擁有您資料與對話的隱私、所有權與控制權。它讓您可以存取開放的網路;因此,您不僅可以與其他 Matrix 使用者聊天。而且非常安全。
Element 能作到這些事情是因為它在 Matrix 上執行,這是一個開放的去中心化通訊的標準。
Element 讓您選擇您要在哪裡託管您的對話來將控制權還給您。在 Element 應用程式中,您可以選擇其他方式來託管:
1. 在由 Matrix 開發者架設的 matrix.org 公開伺服器上取得免費的帳號,或是從數千個由志願者所架設的公開伺服器中選擇
2. 在您自己的硬體上自行架設伺服器並建立帳號
3. 訂閱 Element Matrix 服務託管平台並在自訂伺服氣上註冊帳號
<b>為何選擇 Element</b>
<b>擁有您的資料</b>:您決定您的資料與訊息要放在哪裡。您擁有並控制它,而非某些科技巨頭會挖掘您的資料並將其售予第三方。
<b>開放的即時通訊與協作</b>:您可以與 Matrix 網路中的任何人聊天,不管他們是使用 Element 或其他 Matrix 應用程式都可以,或甚至是其他的訊息系統,如 Slack、IRC 或 XMPP 也都可以。
<b>超級安全</b>:即時的端到端加密(僅有參與對話的人可以解密訊息),以及交叉簽章以驗證對話參與者的裝置。
<b>完整通訊</b>:即時通訊、語音與視訊通話、檔案分享、畫面分享與超多的整合、機器人與小工具。建立聊天室、保持聯繫並完成工作。
<b>無論您身在何處</b>:無論您身在何處,都可以透過 https://app.element.io 來在所有裝置與網路上保持訊息歷史同步。

View File

@ -0,0 +1 @@
安全的去中心化聊天與 VoIP。確保您的資料不受第三方的影響。

View File

@ -0,0 +1 @@
Element曾名為 Riot.im

View File

@ -27,7 +27,7 @@ interface LoginWizard {
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
* @return a [Cancelable]
*/
fun login(login: String,
password: String,

View File

@ -35,6 +35,11 @@ interface UserService {
*/
fun getUser(userId: String): User?
/**
* Try to resolve user from known users, or using profile api
*/
fun resolveUser(userId: String, callback: MatrixCallback<User>)
/**
* Search list of users on server directory.
* @param search the searched term

View File

@ -1204,7 +1204,7 @@ internal class DefaultVerificationService @Inject constructor(
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
?.values?.map { it.deviceId } ?: emptyList()
?.values?.map { it.deviceId }.orEmpty()
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }

View File

@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.user.UserStore
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.task.launchToCallback
@ -49,6 +50,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
private val deleteThreePidTask: DeleteThreePidTask,
private val pendingThreePidMapper: PendingThreePidMapper,
private val userStore: UserStore,
private val fileUploader: FileUploader) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
@ -70,17 +72,17 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
}
override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return setDisplayNameTask
.configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, matrixCallback) {
setDisplayNameTask.execute(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName))
userStore.updateDisplayName(userId, newDisplayName)
}
}
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
userStore.updateAvatar(userId, response.contentUri)
}
}

View File

@ -103,7 +103,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
?: emptyList()
.orEmpty()
}
}
}

View File

@ -28,7 +28,7 @@ internal class RoomTypingUsersHandler @Inject constructor(@UserId private val us
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList()
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()
val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo(

View File

@ -37,6 +37,6 @@ internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTrac
}
override fun getTypingUsers(roomId: String): List<SenderInfo> {
return typingUsers[roomId] ?: emptyList()
return typingUsers[roomId].orEmpty()
}
}

View File

@ -19,10 +19,13 @@ package org.matrix.android.sdk.internal.session.user
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask
import org.matrix.android.sdk.internal.session.user.model.SearchUserTask
import org.matrix.android.sdk.internal.task.TaskExecutor
@ -32,12 +35,40 @@ import javax.inject.Inject
internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource,
private val searchUserTask: SearchUserTask,
private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask,
private val getProfileInfoTask: GetProfileInfoTask,
private val taskExecutor: TaskExecutor) : UserService {
override fun getUser(userId: String): User? {
return userDataSource.getUser(userId)
}
override fun resolveUser(userId: String, callback: MatrixCallback<User>) {
val known = getUser(userId)
if (known != null) {
callback.onSuccess(known)
} else {
val params = GetProfileInfoTask.Params(userId)
getProfileInfoTask
.configureWith(params) {
this.callback = object : MatrixCallback<JsonDict> {
override fun onSuccess(data: JsonDict) {
callback.onSuccess(
User(
userId,
data[ProfileService.DISPLAY_NAME_KEY] as? String,
data[ProfileService.AVATAR_URL_KEY] as? String)
)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
}
override fun getUserLive(userId: String): LiveData<Optional<User>> {
return userDataSource.getUserLive(userId)
}

View File

@ -18,12 +18,15 @@ package org.matrix.android.sdk.internal.session.user
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.internal.database.model.UserEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal interface UserStore {
suspend fun createOrUpdate(userId: String, displayName: String? = null, avatarUrl: String? = null)
suspend fun updateAvatar(userId: String, avatarUrl: String? = null)
suspend fun updateDisplayName(userId: String, displayName: String? = null)
}
internal class RealmUserStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : UserStore {
@ -34,4 +37,20 @@ internal class RealmUserStore @Inject constructor(@SessionDatabase private val m
it.insertOrUpdate(userEntity)
}
}
override suspend fun updateAvatar(userId: String, avatarUrl: String?) {
monarchy.awaitTransaction { realm ->
UserEntity.where(realm, userId).findFirst()?.let {
it.avatarUrl = avatarUrl ?: ""
}
}
}
override suspend fun updateDisplayName(userId: String, displayName: String?) {
monarchy.awaitTransaction { realm ->
UserEntity.where(realm, userId).findFirst()?.let {
it.displayName = displayName ?: ""
}
}
}
}

View File

@ -138,7 +138,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
): LiveData<List<Widget>> {
val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS)
return Transformations.map(widgetsAccountData) {
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes).orEmpty()
}
}

View File

@ -1,79 +1,217 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%1$s ha enviat una imatge.</string>
<string name="notice_room_leave">%1s ha sortit</string>
<string name="notice_room_join">%1s ha entrat</string>
<string name="notice_room_leave">%1$s ha marxat de la sala</string>
<string name="notice_room_join">%1$s s\'ha unit a la sala</string>
<string name="medium_phone_number">Número de telèfon</string>
<string name="medium_email">Correu electrònic</string>
<string name="encrypted_message">Missatge encriptat</string>
<string name="notice_room_invite_no_invitee">la invitació de %s</string>
<string name="encrypted_message">Missatge xifrat</string>
<string name="notice_room_invite_no_invitee">invitació de %s</string>
<string name="notice_room_invite">%1$s ha convidat a %2$s</string>
<string name="notice_room_invite_you">%1$s us ha convidat</string>
<string name="notice_room_invite_you">%1$s t\'ha convidat</string>
<string name="notice_room_reject">%1$s ha rebutjat la invitació</string>
<string name="notice_room_kick">%1$s ha fet fora a %2$s</string>
<string name="notice_display_name_changed_from">%1$s ha canviat el seu nom visible de %2$s a %3$s</string>
<string name="notice_display_name_removed">%1$s ha eliminat el seu nom visible (%2$s)</string>
<string name="notice_room_kick">%1$s ha expulsat %2$s</string>
<string name="notice_display_name_changed_from">%1$s ha canviat el seu nom de visualització de %2$s a %3$s</string>
<string name="notice_display_name_removed">%1$s ha eliminat el seu nom de visualització (era %2$s)</string>
<string name="notice_room_topic_changed">%1$s ha canviat el tema a: %2$s</string>
<string name="notice_room_name_changed">%1$s ha canviat el nom de la sala a: %2$s</string>
<string name="notice_answered_call">%s ha contestat la trucada.</string>
<string name="notice_answered_call">%s ha respost a la trucada.</string>
<string name="notice_ended_call">%s ha finalitzat la trucada.</string>
<string name="notice_room_visibility_invited">tots el membres de la sala, des del punt en què són convidats.</string>
<string name="notice_room_visibility_shared">tots els membres de la sala.</string>
<string name="notice_room_visibility_invited">tots el participants de la sala, des de que són convidats.</string>
<string name="notice_room_visibility_shared">tots els participants de la sala.</string>
<string name="notice_room_visibility_unknown">desconegut (%s).</string>
<string name="notice_end_to_end">%1$s ha activat l\'encriptació d\'extrem a extrem (%2$s)</string>
<string name="notice_end_to_end">%1$s ha activat el xifrat d\'extrem a extrem (%2$s)</string>
<string name="notice_requested_voip_conference">%1$s ha sol·licitat una conferència VoIP</string>
<string name="notice_room_unban">%1$s ha readmès a %2$s</string>
<string name="notice_room_ban">%1$s ha vetat a %2$s</string>
<string name="notice_room_unban">%1$s ha tret el veto a %2$s</string>
<string name="notice_room_ban">%1$s ha vetat %2$s</string>
<string name="notice_room_withdraw">%1$s ha retirat la invitació de %2$s</string>
<string name="notice_avatar_url_changed">%1$s ha canviat el seu avatar</string>
<string name="notice_made_future_room_visibility">%1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara</string>
<string name="notice_room_visibility_joined">tots els membres de la sala, des del punt en què hi entrin.</string>
<string name="notice_made_future_room_visibility">%1$s ha establert la visibilitat de l\'historial futur de la sala a %2$s</string>
<string name="notice_room_visibility_joined">tots els participants de la sala, des de que s\'hi uneixen.</string>
<string name="notice_room_visibility_world_readable">qualsevol.</string>
<string name="notice_voip_started">S\'ha iniciat la conferència VoIP</string>
<string name="notice_voip_finished">S\'ha finalitzat la conferència de veu IP</string>
<string name="notice_avatar_changed_too">(s\'ha canviat també l\'avatar)</string>
<string name="notice_voip_finished">Ha finalitzat la conferència VoIP</string>
<string name="notice_avatar_changed_too">(també ha canviat l\'avatar)</string>
<string name="notice_room_name_removed">%1$s ha eliminat el nom de la sala</string>
<string name="notice_room_topic_removed">%1$s ha eliminat el tema de la sala</string>
<string name="notice_profile_change_redacted">%1$s ha actualitzat el seu perfil %2$s</string>
<string name="notice_room_third_party_invite">%1$s ha enviat una invitació a %2$s per a entrar a la sala</string>
<string name="notice_room_third_party_registered_invite">%1$s ha acceptat la invitació per a %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** No s\'ha pogut desencriptar: %s **</string>
<string name="notice_room_third_party_invite">%1$s ha enviat una invitació a %2$s perquè s\'uneixi a la sala</string>
<string name="notice_room_third_party_registered_invite">%1$s ha acceptat la invitació de %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** No s\'ha pogut desxifrar: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">El dispositiu del remitent no ens ha enviat les claus per aquest missatge.</string>
<string name="could_not_redact">No s\'ha pogut redactar</string>
<string name="unable_to_send_message">No s\'ha pogut enviar el missatge</string>
<string name="message_failed_to_upload">No s\'ha pogut pujar la imatge</string>
<string name="network_error">S\'ha produït un error de xarxa</string>
<string name="matrix_error">S\'ha produït un error de Matrix</string>
<string name="room_error_join_failed_empty_room">Actualment no es pot tornar a entrar a una sala buida.</string>
<string name="notice_display_name_set">%1$s a canviat el seu nom visible a %2$s</string>
<string name="notice_placed_video_call">%s ha iniciat una trucada de vídeo.</string>
<string name="notice_placed_voice_call">%s ha iniciat una trucada de veu.</string>
<string name="network_error">Error de xarxa</string>
<string name="matrix_error">Error de Matrix</string>
<string name="room_error_join_failed_empty_room">Ara per ara no és possible tornar a unir-se a una sala buida.</string>
<string name="notice_display_name_set">%1$s a canviat el seu nom de visualització a %2$s</string>
<string name="notice_placed_video_call">%s ha realitzat una videotrucada.</string>
<string name="notice_placed_voice_call">%s ha realitzat una trucada de veu.</string>
<!-- Room display name -->
<string name="room_displayname_invite_from">Convidat per %s</string>
<string name="room_displayname_room_invite">Convideu a la sala</string>
<string name="room_displayname_invite_from">Invitació de %s</string>
<string name="room_displayname_room_invite">Convida a la sala</string>
<string name="room_displayname_two_members">%1$s i %2$s</string>
<string name="room_displayname_empty_room">Sala buida</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s i 1 altre</item>
<item quantity="other">%1$s i %2$d altres</item>
</plurals>
<string name="summary_user_sent_sticker">%1$s ha enviat un adhesiu.</string>
</resources>
<string name="notice_direct_room_update">%s s\'ha actualitzat aquí.</string>
<string name="notice_direct_room_update_by_you">Ho has actualitzat aquí.</string>
<string name="key_verification_request_fallback_message">%s està sol·licitant la verificació de la teva clau, però el teu client no admet la verificació de clau des del xat. Hauràs d\'utilitzar la verificació de claus heretada per fer la verificació.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Has activat el xifrat d\'extrem a extrem (algorisme %1$s no reconegut).</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s ha activat el xifrat d\'extrem a extrem (algorisme %2$s no reconegut).</string>
<string name="notice_end_to_end_ok_by_you">Has activat el xifrat d\'extrem a extrem.</string>
<string name="notice_end_to_end_ok">%1$s ha activat el xifrat d\'extrem a extrem.</string>
<string name="notice_direct_room_guest_access_forbidden_by_you">Has impedit que els convidats es puguin unir a la sala.</string>
<string name="notice_direct_room_guest_access_forbidden">%1$s ha impedit que els convidats es puguin unir a la sala.</string>
<string name="notice_room_guest_access_forbidden_by_you">Has impedit que els convidats es puguin unir a la sala.</string>
<string name="notice_room_guest_access_forbidden">%1$s ha impedit que els convidats es puguin unir a la sala.</string>
<string name="notice_direct_room_guest_access_can_join_by_you">Has permès que els convidats s\'uneixin aquí.</string>
<string name="notice_direct_room_guest_access_can_join">%1$s ha permès que els convidats s\'uneixin aquí.</string>
<string name="notice_room_guest_access_can_join_by_you">Has permès que els convidats s\'uneixin a la sala.</string>
<string name="notice_room_guest_access_can_join">%1$s ha permès que els convidats s\'uneixin a la sala.</string>
<string name="notice_room_canonical_alias_unset_by_you">Has eliminat l\'adreça principal d\'aquesta sala.</string>
<string name="notice_room_canonical_alias_unset">%1$s ha eliminat l\'adreça principal d\'aquesta sala.</string>
<string name="notice_room_canonical_alias_set_by_you">Has establert l\'adreça principal d\'aquesta sala a %1$s.</string>
<string name="notice_room_canonical_alias_set">%1$s ha establert l\'adreça principal d\'aquesta sala a %2$s.</string>
<string name="notice_room_aliases_added_and_removed_by_you">Has afegit %1$s i has eliminat %2$s d\'aquesta sala (adreces).</string>
<string name="notice_room_aliases_added_and_removed">%1$s ha afegit %2$s i ha eliminat %3$s d\'aquesta sala (adreces).</string>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">Has eliminat l\'adreça %1$s d\'aquesta sala.</item>
<item quantity="other">Has eliminat les adreces %1$s d\'aquesta sala.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s ha eliminat l\'adreça %2$s d\'aquesta sala.</item>
<item quantity="other">%1$s ha eliminat les adreces %3$s d\'aquesta sala.</item>
</plurals>
<plurals name="notice_room_aliases_added_by_you">
<item quantity="one">Has afegit l\'adreça %1$s a aquesta sala.</item>
<item quantity="other">Has afegit les adreces %1$s a aquesta sala.</item>
</plurals>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s ha afegit l\'adreça %2$s a aquesta sala.</item>
<item quantity="other">%1$s ha afegit les adreces %2$s a aquesta sala.</item>
</plurals>
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Has revocat la invitació de %1$s perquè s\'uneixi a la sala. Motiu: %2$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s ha revocat la invitació de %2$s perquè s\'uneixi a la sala. Motiu: %3$s</string>
<string name="notice_direct_room_third_party_revoked_invite_by_you">Has revocat la invitació de %1$s</string>
<string name="notice_direct_room_third_party_revoked_invite">%1$s ha revocat la invitació de %2$s</string>
<string name="notice_room_third_party_revoked_invite_by_you">Has revocat la invitació de %1$s perquè s\'uneixi a la sala</string>
<string name="notice_room_third_party_revoked_invite">%1$s ha revocat la invitació de %2$s perquè s\'uneixi a la sala</string>
<string name="notice_room_withdraw_with_reason_by_you">Has retirat la invitació de %1$s. Motiu: %2$s</string>
<string name="notice_room_withdraw_with_reason">%1$s ha retirat la invitació de %2$s. Motiu: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Has acceptat la invitació de %1$s. Motiu: %2$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s ha acceptat la invitació de %2$s. Motiu: %3$s</string>
<string name="notice_room_third_party_invite_with_reason_by_you">Has enviat una invitació a %1$s perquè s\'uneixi a la sala. Motiu: %2$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s ha enviat una invitació a %2$s perquè s\'uneixi a la sala. Motiu: %3$s</string>
<string name="notice_room_ban_with_reason_by_you">Has vetat %1$s. Motiu: %2$s</string>
<string name="notice_room_ban_with_reason">%1$s ha vetat %2$s. Motiu: %3$s</string>
<string name="notice_room_unban_with_reason_by_you">Has tret el veto a %1$s. Motiu: %2$s</string>
<string name="notice_room_unban_with_reason">%1$s ha tret el veto a %2$s. Motiu: %3$s</string>
<string name="notice_room_ban_by_you">Has vetat %1$s</string>
<string name="notice_room_leave_with_reason_by_you">Has marxat de la sala. Motiu: %1$s</string>
<string name="notice_room_leave_with_reason">%1$s ha marxat de la sala. Motiu: %2$s</string>
<string name="notice_direct_room_leave_by_you">Has marxat de la sala</string>
<string name="notice_direct_room_leave">%1$s ha marxat de la sala</string>
<string name="notice_room_leave_by_you">Has marxat de la sala</string>
<string name="notice_room_kick_by_you">Has expulsat %1$s</string>
<string name="notice_room_kick_with_reason_by_you">Has expulsat %1$s. Motiu: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s ha expulsat %2$s. Motiu: %3$s</string>
<string name="notice_room_reject_with_reason_by_you">Has rebutjat la invitació. Motiu: %1$s</string>
<string name="notice_room_reject_with_reason">%1$s ha rebutjat la invitació. Motiu: %2$s</string>
<string name="notice_direct_room_leave_with_reason_by_you">Has marxat. Motiu: %1$s</string>
<string name="notice_direct_room_leave_with_reason">%1$s ha marxat. Motiu: %2$s</string>
<string name="notice_direct_room_join_with_reason_by_you">T\'has unit. Motiu: %1$s</string>
<string name="notice_direct_room_join_with_reason">%1$s s\'ha unit. Motiu: %2$s</string>
<string name="notice_room_join_with_reason_by_you">T\'has unit a la sala. Motiu: %1$s</string>
<string name="notice_room_join_with_reason">%1$s s\'ha unit a la sala. Motiu: %2$s</string>
<string name="notice_room_invite_you_with_reason">%1$s t\'ha convidat. Motiu: %2$s</string>
<string name="notice_room_invite_with_reason_by_you">Has convidat %1$s. Motiu: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s ha convidat %2$s. Motiu: %3$s</string>
<string name="notice_room_invite_no_invitee_with_reason_by_you">La teva invitació. Motiu: %1$s</string>
<string name="notice_room_invite_no_invitee_with_reason">la invitació de %1$s. Motiu: %2$s</string>
<string name="clear_timeline_send_queue">Esborra la cua d\'enviament</string>
<string name="event_status_sending_message">Enviant missatge…</string>
<string name="initial_sync_start_importing_account_data">Sincronització inicial:
\nImportant dades del compte</string>
<string name="initial_sync_start_importing_account_groups">Sincronització inicial:
\nImportant comunitats</string>
<string name="initial_sync_start_importing_account_left_rooms">Sincronització inicial:
\nImportant sales que deixat</string>
<string name="initial_sync_start_importing_account">Sincronització inicial:
\nImportant compte…</string>
<string name="initial_sync_start_importing_account_crypto">Sincronització inicial:
\nImportant xifrat</string>
<string name="initial_sync_start_importing_account_rooms">Sincronització inicial:
\nImportant sales</string>
<string name="initial_sync_start_importing_account_invited_rooms">Sincronització inicial:
\nImportant sales on hi estàs convidat</string>
<string name="initial_sync_start_importing_account_joined_rooms">Sincronització inicial:
\nImportant sales on hi estàs unit</string>
<string name="notice_power_level_diff">%1$s de %2$s a %3$s</string>
<string name="notice_power_level_changed">%1$s ha canviat el nivell d\'autoritat de %2$s.</string>
<string name="notice_power_level_changed_by_you">Has canviat el nivell d\'autoritat de %1$s.</string>
<string name="power_level_custom_no_value">Personalitzat</string>
<string name="power_level_custom">Personalitzat (%1$d)</string>
<string name="power_level_default">Predeterminat</string>
<string name="power_level_moderator">Moderador</string>
<string name="power_level_admin">Administrador</string>
<string name="notice_widget_modified_by_you">Has modificat el giny %1$s</string>
<string name="notice_widget_modified">%1$s ha modificat el giny %2$s</string>
<string name="notice_widget_removed_by_you">Has eliminat el giny %1$s</string>
<string name="notice_widget_removed">%1$s ha eliminat el giny %2$s</string>
<string name="notice_widget_added_by_you">Has afegit el giny %1$s</string>
<string name="notice_widget_added">%1$s ha afegit el giny %2$s</string>
<string name="notice_room_third_party_registered_invite_by_you">Has acceptat la invitació de %1$s</string>
<string name="notice_direct_room_third_party_invite_by_you">Has convidat a %1$s</string>
<string name="notice_direct_room_third_party_invite">%1$s ha convidat a %2$s</string>
<string name="notice_room_third_party_invite_by_you">Has enviat una invitació a %1$s perquè s\'uneixi a la sala</string>
<string name="notice_profile_change_redacted_by_you">Has actualitzat el teu perfil %1$s</string>
<string name="notice_event_redacted_by_with_reason">Missatge eliminat per %1$s [motiu: %2$s]</string>
<string name="notice_event_redacted_with_reason">Missatge eliminat [motiu: %1$s]</string>
<string name="notice_event_redacted_by">Missatge eliminat per %1$s</string>
<string name="notice_event_redacted">Missatge eliminat</string>
<string name="notice_room_avatar_removed_by_you">Has eliminat l\'avatar de la sala</string>
<string name="notice_room_avatar_removed">%1$s ha eliminat l\'avatar de la sala</string>
<string name="notice_room_topic_removed_by_you">Has eliminat el tema de la sala</string>
<string name="notice_room_name_removed_by_you">Has eliminat el nom de la sala</string>
<string name="notice_requested_voip_conference_by_you">Has sol·licitat una conferència VoIP</string>
<string name="notice_room_update_by_you">Has actualitzat aquesta sala.</string>
<string name="notice_room_update">%s ha actualitzat aquesta sala.</string>
<string name="notice_end_to_end_by_you">Has activat el xifrat d\'extrem a extrem (%1$s)</string>
<string name="notice_made_future_direct_room_visibility_by_you">Has establert la visibilitat dels missatges futurs a %1$s</string>
<string name="notice_made_future_direct_room_visibility">%1$s ha establert la visibilitat dels missatges futurs a %2$s</string>
<string name="notice_made_future_room_visibility_by_you">Has establert la visibilitat de l\'historial futur de la sala a %1$s</string>
<string name="notice_ended_call_by_you">Has finalitzat la trucada.</string>
<string name="notice_answered_call_by_you">Has respost a la trucada.</string>
<string name="notice_call_candidates_by_you">Has enviat dades per configurar la trucada.</string>
<string name="notice_call_candidates">%s ha enviat dades per configurar la trucada.</string>
<string name="notice_placed_voice_call_by_you">Has realitzat una trucada de veu.</string>
<string name="notice_placed_video_call_by_you">Has realitzat una videotrucada.</string>
<string name="notice_room_name_changed_by_you">Has canviat el nom de la sala a: %1$s</string>
<string name="notice_room_avatar_changed_by_you">Has canviat l\'avatar de la sala</string>
<string name="notice_room_avatar_changed">%1$s ha canviat l\'avatar de la sala</string>
<string name="notice_room_topic_changed_by_you">Has canviat el tema a: %1$s</string>
<string name="notice_display_name_removed_by_you">Has eliminat el teu nom de visualització (era %1$s)</string>
<string name="notice_display_name_changed_from_by_you">Has canviat el teu nom de visualització de %1$s a %2$s</string>
<string name="notice_display_name_set_by_you">Has canviat el teu nom de visualització a %1$s</string>
<string name="notice_avatar_url_changed_by_you">Has canviat el teu avatar</string>
<string name="notice_room_withdraw_by_you">Has retirat la invitació de %1$s</string>
<string name="notice_room_unban_by_you">Has tret el veto a %1$s</string>
<string name="notice_room_reject_by_you">Has rebutjat la invitació</string>
<string name="notice_direct_room_created_by_you">Has creat la discussió</string>
<string name="notice_direct_room_created">%1$s ha creat la discussió</string>
<string name="notice_direct_room_join_by_you">T\'has unit</string>
<string name="notice_direct_room_join">%1$s s\'ha unit</string>
<string name="notice_room_join_by_you">T\'has unit a la sala</string>
<string name="notice_room_invite_by_you">Has convidat a %1$s</string>
<string name="notice_room_created_by_you">Has creat la sala</string>
<string name="notice_room_created">%1$s ha creat la sala</string>
<string name="notice_room_invite_no_invitee_by_you">La teva invitació</string>
<string name="summary_you_sent_sticker">Has enviat un adhesiu.</string>
<string name="summary_you_sent_image">Has enviat una imatge.</string>
</resources>

View File

@ -1,9 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%1$s envió una imagen.</string>
<string name="notice_room_invite_no_invitee">la invitación de %s</string>
<string name="notice_room_invite">%1$s invitó a %2$s</string>
<string name="notice_room_invite_you">%1$s te ha invitado</string>
@ -30,61 +28,44 @@
<string name="notice_room_visibility_shared">todos los miembros de la sala.</string>
<string name="notice_room_visibility_world_readable">todos.</string>
<string name="notice_room_visibility_unknown">desconocido (%s).</string>
<string name="notice_end_to_end">%1$s activó el cifrado de extremo a extremo (%2$s)</string>
<string name="notice_end_to_end">%1$s ha activado la encriptación de Extremo-a-Extremo (%2$s)</string>
<string name="notice_requested_voip_conference">%1$s solicitó una conferencia de vozIP</string>
<string name="notice_voip_started">conferencia de vozIP iniciada</string>
<string name="notice_voip_finished">conferencia de vozIP finalizada</string>
<string name="notice_avatar_changed_too">(el avatar también se cambió)</string>
<string name="notice_room_name_removed">%1$s eliminó el nombre de la sala</string>
<string name="notice_room_topic_removed">%1$s eliminó el tema de la sala</string>
<string name="notice_profile_change_redacted">%1$s actualizó su perfil %2$s</string>
<string name="notice_room_third_party_invite">%1$s invitó a %2$s a unirse a la sala</string>
<string name="notice_room_third_party_registered_invite">%1$s aceptó la invitación para %2$s</string>
<string name="notice_crypto_unable_to_decrypt">** No es posible descifrar: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">El dispositivo emisor no nos ha enviado las claves para este mensaje.</string>
<!-- Room Screen -->
<string name="could_not_redact">No se pudo redactar</string>
<string name="unable_to_send_message">No es posible enviar el mensaje</string>
<string name="message_failed_to_upload">No se pudo cargar la imagen</string>
<!-- general errors -->
<string name="network_error">Error de red</string>
<string name="matrix_error">Error de Matrix</string>
<!-- Home Screen -->
<!-- Last seen time -->
<!-- call events -->
<!-- room error messages -->
<string name="room_error_join_failed_empty_room">Actualmente no es posible volver a unirse a una sala vacía.</string>
<string name="encrypted_message">Mensaje cifrado</string>
<string name="encrypted_message">Mensaje encriptado</string>
<!-- medium friendly name -->
<string name="medium_email">Dirección de correo electrónico</string>
<string name="medium_phone_number">Número telefónico</string>
<string name="summary_user_sent_sticker">%1$s envió una pegatina.</string>
<!-- Room display name -->
<string name="room_displayname_invite_from">Invitación de %s</string>
<string name="room_displayname_room_invite">Invitación a Sala</string>
<string name="room_displayname_two_members">%1$s y %2$s</string>
<string name="room_displayname_empty_room">Sala vacía</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s y 1 otro</item>
<item quantity="other">%1$s y %2$d otros</item>
</plurals>
<string name="notice_event_redacted">Mensaje eliminado</string>
<string name="notice_event_redacted_by">Mensaje eliminado por %1$s</string>
<string name="notice_event_redacted_with_reason">Mensaje eliminado [motivo: %1$s]</string>
@ -98,10 +79,8 @@
\nImportando Comunidades</string>
<string name="initial_sync_start_importing_account_data">Sincronización Inicial:
\nImportando Datos de la Cuenta</string>
<string name="event_status_sending_message">Enviando mensaje…</string>
<string name="clear_timeline_send_queue">Borrar cola de envío</string>
<string name="notice_room_invite_with_reason">%1$s ha invitado a %2$s. Razón: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s te ha invitado. Razón: %2$s</string>
<string name="notice_room_join_with_reason">%1$s se ha unido. Razón: %2$s</string>
@ -111,9 +90,7 @@
<string name="notice_room_ban_with_reason">%1$s ha baneado a %2$s. Razón: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s ha aceptado la invitación para %2$s. Razón: %3$s</string>
<string name="notice_room_canonical_alias_unset">%1$s ha eliminado la dirección principal para esta sala.</string>
<string name="notice_room_update">%s ha actualizado la sala.</string>
<string name="initial_sync_start_importing_account_crypto">Sincronización Inicial:
\nImportando criptografía</string>
<string name="initial_sync_start_importing_account_joined_rooms">Sincronización Inicial:
@ -127,32 +104,25 @@
<string name="notice_room_third_party_invite_with_reason">%1$s envió una invitación a %2$s para que se una a la sala. Razón: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revocó la invitación de %2$s para unirse a la sala. Razón: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s ha retirado la invitación de %2$s. Razón: %3$s</string>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s ha añadido %2$s como alias de esta sala.</item>
<item quantity="other">%1$s ha añadido %2$s como alias de esta sala.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s ha quitado %2$s como alias de esta habitación.</item>
<item quantity="other">%1$s ha quitado %2$s como alias de esta habitación.</item>
<item quantity="one">%1$s ha quitado %2$s como alias de esta sala.</item>
<item quantity="other">%1$s ha quitado %2$s como alias de esta sala.</item>
</plurals>
<string name="notice_room_canonical_alias_set">%1$s ha establecido la dirección principal de esta sala a %2$s.</string>
<string name="notice_room_guest_access_can_join">%1$s ha permitido que los invitados se unan a la sala.</string>
<string name="notice_room_guest_access_forbidden">%1$s ha impedido que los invitados se unan a la sala.</string>
<string name="notice_end_to_end_ok">%1$s ha activado la encriptación extremo a extremo.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s ha activado la encriptación de extremo a extremo (algoritmo no reconocido %2$s).</string>
<string name="key_verification_request_fallback_message">%s solicita verificar su clave, pero su cliente no soporta la verificación de la clave en chat. Necesitará usar la verificación de claves clásica para poder verificar las claves.</string>
<string name="summary_you_sent_image">Enviaste una imagen.</string>
<string name="summary_you_sent_sticker">Enviaste un sticker.</string>
<string name="notice_room_invite_no_invitee_by_you">Tu invitación</string>
<string name="notice_room_created">%1$s creó la habitación</string>
<string name="notice_room_created_by_you">Tu creaste la habitación</string>
<string name="notice_room_created">%1$s creó la sala</string>
<string name="notice_room_created_by_you">Creaste la sala</string>
<string name="notice_room_invite_by_you">Invitaste a %1$s</string>
<string name="notice_room_join_by_you">Te uniste a la Sala</string>
<string name="notice_room_leave_by_you">Dejaste la Sala</string>
@ -167,8 +137,8 @@
<string name="notice_display_name_removed_by_you">Quitaste tu nombre para mostrar (era %1$s)</string>
<string name="notice_room_topic_changed_by_you">Cambiaste el tema a: %1$s</string>
<string name="notice_room_avatar_changed">%1$s cambió el avatar de la sala</string>
<string name="notice_room_avatar_changed_by_you">Cambiaste el avatar de la habitación</string>
<string name="notice_room_name_changed_by_you">Cambiaste el nombre de la habitación a: %1$s</string>
<string name="notice_room_avatar_changed_by_you">Cambiaste el avatar de la sala</string>
<string name="notice_room_name_changed_by_you">Cambiaste el nombre de la sala a: %1$s</string>
<string name="notice_placed_video_call_by_you">Hiciste una videollamada.</string>
<string name="notice_placed_voice_call_by_you">Hiciste una llamada de voz.</string>
<string name="notice_call_candidates">%s envió datos para configurar la llamada.</string>
@ -176,40 +146,35 @@
<string name="notice_answered_call_by_you">Respondiste la llamada.</string>
<string name="notice_ended_call_by_you">Terminaste la llamada.</string>
<string name="notice_made_future_room_visibility_by_you">Hiciste visible el futuro historial de la %1$s</string>
<string name="notice_end_to_end_by_you">Activó el cifrado de un extremo a otro (%1$s)</string>
<string name="notice_room_update_by_you">Has mejorado esta habitación.</string>
<string name="notice_end_to_end_by_you">Has activado la encriptación de Extremo-a-Extremo (%1$s)</string>
<string name="notice_room_update_by_you">Has actualizado esta sala.</string>
<string name="notice_requested_voip_conference_by_you">Solicitaste una conferencia de VoIP</string>
<string name="notice_room_name_removed_by_you">Quitaste el nombre de la sala</string>
<string name="notice_room_topic_removed_by_you">Quitaste el tema de la sala</string>
<string name="notice_room_avatar_removed">%1$s eliminó el avatar de la habitación</string>
<string name="notice_room_avatar_removed_by_you">Quitaste el avatar de la habitación</string>
<string name="notice_room_avatar_removed">%1$s eliminó el avatar de la sala</string>
<string name="notice_room_avatar_removed_by_you">Quitaste el avatar de la sala</string>
<string name="notice_profile_change_redacted_by_you">Actualizaste tu perfil %1$s</string>
<string name="notice_room_third_party_invite_by_you">Enviaste una invitación a %1$s para unirse a la sala</string>
<string name="notice_room_third_party_revoked_invite_by_you">Revocaste la invitación para que %1$s se una a la sala</string>
<string name="notice_room_third_party_registered_invite_by_you">Aceptaste la invitación para %1$s</string>
<string name="notice_widget_added">%1$s agrego el widget %2$s</string>
<string name="notice_widget_added_by_you">Agregaste el widget %1$s</string>
<string name="notice_widget_removed">%1$s eliminó el widget %2$s</string>
<string name="notice_widget_removed_by_you">Quitaste el widget %1$s</string>
<string name="notice_widget_modified">%1$s modifico el widget %2$s</string>
<string name="notice_widget_modified_by_you">Modificaste el widget %1$s</string>
<string name="power_level_admin">Administrador</string>
<string name="power_level_moderator">Moderador</string>
<string name="power_level_default">Por defecto</string>
<string name="power_level_custom">Personalizado (%1$d)</string>
<string name="power_level_custom_no_value">Personalizado</string>
<string name="notice_power_level_changed_by_you">Cambiaste el nivel de potencia de %1$s.</string>
<string name="notice_power_level_changed">%1$s cambió el nivel de potencia de %2$s.</string>
<string name="notice_power_level_diff">%1$s de %2$s a %3$s</string>
<string name="notice_room_invite_no_invitee_with_reason_by_you">Tu invitación. Razón: %1$s</string>
<string name="notice_room_invite_with_reason_by_you">"nvitaste a %1$s. Razón: %2$s"</string>
<string name="notice_room_join_with_reason_by_you">Te uniste a la habitación. Razón: %1$s</string>
<string name="notice_room_leave_with_reason_by_you">Dejaste la habitación. Razón: %1$s</string>
<string name="notice_room_invite_with_reason_by_you">Invitaste a %1$s. Razón: %2$s</string>
<string name="notice_room_join_with_reason_by_you">Te uniste a la sala. Razón: %1$s</string>
<string name="notice_room_leave_with_reason_by_you">Dejaste la sala. Razón: %1$s</string>
<string name="notice_room_reject_with_reason_by_you">Rechazaste la invitación. Razón: %1$s</string>
<string name="notice_room_kick_with_reason_by_you">Pateaste a %1$s. Motivo: %2$s</string>
<string name="notice_room_unban_with_reason_by_you">Has desactivado a %1$s. Motivo: %2$s</string>
@ -218,27 +183,42 @@
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Revocaste la invitación para que %1$s se una a la sala. Motivo: %2$s</string>
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Aceptaste la invitación para %1$s. Motivo: %2$s</string>
<string name="notice_room_withdraw_with_reason_by_you">Retiró la invitación de %1$s\'s. Motivo: %2$s</string>
<plurals name="notice_room_aliases_added_by_you">
<item quantity="one">Agregaste %1$s como dirección para esta sala.</item>
<item quantity="other">Agregaste %1$s como direcciones para esta sala.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">Quitaste %1$s como dirección para esta sala.</item>
<item quantity="other">Quitaste %1$s como direcciones para esta sala.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>
<string name="notice_room_aliases_added_and_removed">%1$s añadió %2$s y eliminó %3$s como alias para esta sala.</string>
<string name="notice_room_aliases_added_and_removed_by_you">Agregaste %1$s y quitaste %2$s como direcciones para esta sala.</string>
<string name="notice_room_canonical_alias_set_by_you">Estableciste la dirección principal de esta sala en %1$s.</string>
<string name="notice_room_canonical_alias_unset_by_you">Quitaste la dirección principal de esta sala.</string>
<string name="notice_room_guest_access_can_join_by_you">Ha permitido que los invitados se unan a la sala.</string>
<string name="notice_room_guest_access_forbidden_by_you">Ha impedido que los invitados se unan a la sala.</string>
<string name="notice_end_to_end_ok_by_you">Activó el cifrado de extremo a extremo.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Activó el cifrado de un extremo a otro (algoritmo %1$s no reconocido).</string>
</resources>
<string name="notice_end_to_end_ok_by_you">Tu has activado la encriptación de Extremo-a-Extremo.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Has activado la encriptación de Extremo-a-Extremo (algoritmo %1$s no reconocido).</string>
<string name="notice_direct_room_guest_access_forbidden_by_you">Has impedido que invitados se unan a la sala.</string>
<string name="notice_direct_room_guest_access_can_join_by_you">Has permitido a invitados unirse aquí.</string>
<string name="notice_direct_room_leave_with_reason_by_you">Te has ido. Razón: %1$s</string>
<string name="notice_direct_room_third_party_revoked_invite_by_you">Has revocado la invitación de %1$s</string>
<string name="notice_direct_room_third_party_invite_by_you">Has invitado a %1$s</string>
<string name="notice_direct_room_update_by_you">Has actualizado aquí.</string>
<string name="notice_made_future_direct_room_visibility_by_you">Has hecho futuros mensajes visibles a %1$s</string>
<string name="notice_direct_room_leave_by_you">Te saliste de la sala</string>
<string name="notice_direct_room_join_by_you">Te uniste</string>
<string name="notice_direct_room_created_by_you">Creaste la conversación</string>
<string name="notice_direct_room_guest_access_forbidden">%1$s ha impedido que invitados se unan a la sala.</string>
<string name="notice_direct_room_guest_access_can_join">%1$s ha permitido a invitados a unirse aquí.</string>
<string name="notice_direct_room_leave_with_reason">%1$s se ha ido. Razón: %2$s</string>
<string name="notice_direct_room_join_with_reason_by_you">Tu te has unido. Razón: %1$s</string>
<string name="notice_direct_room_join_with_reason">%1$s se ha unido. Razón: %2$s</string>
<string name="notice_direct_room_third_party_revoked_invite">%1$s ha revocado la invitación de %2$s</string>
<string name="notice_direct_room_third_party_invite">%1$s ha invitado %2$s</string>
<string name="notice_direct_room_update">%s ha actualizado aquí.</string>
<string name="notice_made_future_direct_room_visibility">%1$s ha hecho futuros mensajes visibles a %2$s</string>
<string name="notice_direct_room_leave">%1$s ha salido de la sala</string>
<string name="notice_direct_room_join">%1$s se ha unido</string>
<string name="notice_direct_room_created">%1$s ha creado la conversación</string>
</resources>

View File

@ -181,8 +181,8 @@
<item quantity="other">نشانی‌های %1$s را به این اتاق افزودید.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">نشانی %1$s ار از این اتاق برداشتید.</item>
<item quantity="other">نشانی‌های %1$s ار از این اتاق برداشتید.</item>
<item quantity="one">نشانی %1$s را از این اتاق برداشتید.</item>
<item quantity="other">نشانی‌های %1$s را از این اتاق برداشتید.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed_by_you">نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید.</string>
<string name="notice_room_canonical_alias_set_by_you">نشانی اصلی این اتاق را به %1$s تنظیم کردید.</string>

View File

@ -22,10 +22,10 @@
<string name="notice_placed_voice_call">%s hanghívást indított.</string>
<string name="notice_answered_call">%s fogadta a hívást.</string>
<string name="notice_ended_call">%s befejezte a hívást.</string>
<string name="notice_made_future_room_visibility">%1$s láthatóvá tette a jövőbeli előzményeket %2$s számára</string>
<string name="notice_room_visibility_invited">az összes szobatag, onnantól, hogy meg lettek hívva.</string>
<string name="notice_room_visibility_joined">az összes szobatag, onnantól, hogy csatlakoztak.</string>
<string name="notice_room_visibility_shared">az összes szobatag.</string>
<string name="notice_made_future_room_visibility">%1$s láthatóvá tette a jövőbeli előzményeket %2$s</string>
<string name="notice_room_visibility_invited">a szoba összes tagja számára, a meghívásuk időpontjától kezdve.</string>
<string name="notice_room_visibility_joined">a szoba összes tagja számára, a csatlakozásuk időpontjától kezdve.</string>
<string name="notice_room_visibility_shared">az összes szobatag számára.</string>
<string name="notice_room_visibility_world_readable">bárki.</string>
<string name="notice_room_visibility_unknown">ismeretlen (%s).</string>
<string name="notice_end_to_end">%1$s bekapcsolta a végpontok közötti titkosítást (%2$s)</string>
@ -139,4 +139,40 @@
<string name="notice_room_created_by_you">Létrehoztad a szobát</string>
<string name="summary_you_sent_sticker">Matricát küldtél.</string>
<string name="summary_you_sent_image">Képet küldtél.</string>
<string name="power_level_custom_no_value">Saját</string>
<string name="power_level_custom">Saját (%1$d)</string>
<string name="power_level_default">Alapértelmezett</string>
<string name="power_level_moderator">Moderátor</string>
<string name="power_level_admin">Admin</string>
<string name="notice_widget_modified_by_you">Ön megváltoztatta a %1$s kisalkalmazást</string>
<string name="notice_widget_modified">%1$s megváltoztatta a %2$s kisalkalmazást</string>
<string name="notice_widget_removed_by_you">Ön eltávolította a %1$s kisalkalmazást</string>
<string name="notice_widget_removed">%1$s eltávolította a %2$s kisalkalmazást</string>
<string name="notice_widget_added_by_you">Ön hozzáadott egy %1$s kisalkalmazást</string>
<string name="notice_widget_added">%1$s hozzáadott egy %2$s kisalkalmazást</string>
<string name="notice_room_third_party_registered_invite_by_you">Ön elfogadta a meghívót ehhez: %1$s</string>
<string name="notice_direct_room_third_party_revoked_invite_by_you">Ön visszavonta %1$s felhasználó meghívóját</string>
<string name="notice_direct_room_third_party_revoked_invite">%1$s visszavonta %2$s felhasználó meghívóját</string>
<string name="notice_room_third_party_revoked_invite_by_you">Ön visszavonta %1$s felhasználó meghívóját</string>
<string name="notice_direct_room_third_party_invite_by_you">Ön meghívta %1$s felhasználót</string>
<string name="notice_direct_room_third_party_invite">%1$s meghívta %2$s felhasználót</string>
<string name="notice_room_third_party_invite_by_you">Ön meghívót küldött %1$s felhasználónak, hogy csatlakozzon a szobához</string>
<string name="notice_profile_change_redacted_by_you">Ön frissítette a saját profilját %1$s</string>
<string name="notice_room_avatar_removed_by_you">Ön eltávolította a szoba képét</string>
<string name="notice_room_avatar_removed">%1$s eltávolította a szoba képét</string>
<string name="notice_room_topic_removed_by_you">Ön eltávolította a szoba témáját</string>
<string name="notice_room_name_removed_by_you">Ön eltávolította a szoba nevét</string>
<string name="notice_requested_voip_conference_by_you">Ön videókonferencia kezdeményezését kérte</string>
<string name="notice_direct_room_update_by_you">Ön frissítette ezt a szobát.</string>
<string name="notice_direct_room_update">%s frissítette a szobát.</string>
<string name="notice_room_update_by_you">Ön frissítette ezt a szobát.</string>
<string name="notice_end_to_end_by_you">Ön bekapcsolta a végpontok közötti titkosítást (%1$s)</string>
<string name="notice_made_future_direct_room_visibility_by_you">Ön elérhetővé tette a jövőbeni üzeneteket %1$s</string>
<string name="notice_made_future_room_visibility_by_you">Ön elérhetővé tette a jövőbeni üzeneteket %1$s</string>
<string name="notice_made_future_direct_room_visibility">%1$s elérhetővé tette a jövőbeni üzeneteket %2$s</string>
<string name="notice_room_name_changed_by_you">Ön megváltoztatta a szoba nevét erre: %1$s</string>
<string name="notice_display_name_removed_by_you">Ön eltávolította a saját megjelenített nevét (%1$s volt)</string>
<string name="notice_display_name_changed_from_by_you">Ön megváltoztatta a saját megjelenítési nevét erről: %1$s, erre: %2$s</string>
<string name="notice_display_name_set_by_you">Ön beállította a saját megjelenítési nevét erre: %1$s</string>
<string name="notice_room_invite_no_invitee_by_you">Az ön meghívása</string>
</resources>

View File

@ -1,10 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%1$sが画像を送信しました。</string>
<string name="summary_user_sent_sticker">%1$sがスタンプを送信しました。</string>
<string name="notice_room_invite_no_invitee">%sの招待</string>
<string name="notice_room_invite">%1$sが%2$sを招待しました</string>
<string name="notice_room_invite_you">%1$sがあなたを招待しました</string>
@ -29,11 +27,9 @@
<string name="room_displayname_room_invite">部屋への招待</string>
<string name="room_displayname_two_members">%1$sと%2$s</string>
<string name="room_displayname_empty_room">空の部屋</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="other">%1$sと他%2$d名</item>
</plurals>
<string name="notice_made_future_room_visibility">%1$sは、今後の部屋履歴を%2$sに表示させました</string>
<string name="notice_room_visibility_invited">部屋のメンバー全員、招待された時点から。</string>
<string name="notice_room_visibility_joined">部屋のメンバー全員、参加した時点から。</string>
@ -41,34 +37,50 @@
<string name="notice_room_visibility_world_readable">誰でも。</string>
<string name="notice_room_visibility_unknown">不明 (%s)。</string>
<string name="notice_end_to_end">%1$s がエンドツーエンド暗号化を有効にしました (%2$s)</string>
<string name="notice_requested_voip_conference">%1$s がVoIP会議をリクエストしました</string>
<string name="notice_voip_started">VoIP会議が開始されました</string>
<string name="notice_voip_finished">VoIP会議が終了しました</string>
<string name="notice_avatar_changed_too">(アバターも変更された)</string>
<string name="notice_room_name_removed">%1$s が部屋名を削除しました</string>
<string name="notice_room_topic_removed">%1$s がルームトピックを削除しました</string>
<string name="notice_profile_change_redacted">%1$s がプロフィール %2$s を更新しました</string>
<string name="notice_room_third_party_invite">%1$s は %2$s に部屋に参加するよう招待状を送りました</string>
<string name="notice_room_third_party_registered_invite">%1$sは%2$sの招待を受け入れました</string>
<string name="notice_crypto_unable_to_decrypt">** 解読できません: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">送信者の端末からこのメッセージのキーが送信されていません。</string>
<string name="could_not_redact">修正できませんでした</string>
<string name="unable_to_send_message">メッセージを送信できません</string>
<string name="message_failed_to_upload">画像のアップロードに失敗しました</string>
<string name="network_error">ネットワークエラー</string>
<string name="matrix_error">Matrixエラー</string>
<string name="room_error_join_failed_empty_room">現在空の部屋に再参加することはできません。</string>
<string name="encrypted_message">暗号化されたメッセージ</string>
<string name="medium_email">メールアドレス</string>
<string name="medium_phone_number">電話番号</string>
</resources>
<string name="notice_room_avatar_changed_by_you">ルームのアバターを変更しました</string>
<string name="notice_room_avatar_changed">%1$sがルームのアバターを変更しました</string>
<string name="notice_room_topic_changed_by_you">トピックを%1$sに変更しました</string>
<string name="notice_display_name_removed_by_you">表示名を削除しました(%1$sでした)</string>
<string name="notice_display_name_changed_from_by_you">表示名を%1$sから%2$sに変更しました</string>
<string name="notice_display_name_set_by_you">表示名を%1$sに設定しました</string>
<string name="notice_avatar_url_changed_by_you">アバターを変更しました</string>
<string name="notice_room_withdraw_by_you">%1$sの招待を取り下げました</string>
<string name="notice_room_ban_by_you">%1$sをBANしました</string>
<string name="notice_room_unban_by_you">%1$sのBANを解除しました</string>
<string name="notice_room_kick_by_you">%1$sを退出させました</string>
<string name="notice_room_reject_by_you">招待を拒否しました</string>
<string name="notice_direct_room_leave_by_you">ルームから退出しました</string>
<string name="notice_direct_room_leave">%1$sがルームから退出しました</string>
<string name="notice_room_leave_by_you">ルームから退出しました</string>
<string name="notice_direct_room_join_by_you">参加しました</string>
<string name="notice_direct_room_join">%1$sが参加しました</string>
<string name="notice_room_join_by_you">ルームに参加しました</string>
<string name="notice_room_invite_by_you">%1$sを招待しました</string>
<string name="notice_direct_room_created_by_you">ディスカッションを作成しました</string>
<string name="notice_direct_room_created">%1$sがディスカッションを作成しました</string>
<string name="notice_room_created_by_you">ルームを作成しました</string>
<string name="notice_room_created">%1$sがルームを作成しました</string>
<string name="notice_room_invite_no_invitee_by_you">招待</string>
<string name="summary_you_sent_sticker">ステッカーを送信しました。</string>
<string name="summary_you_sent_image">画像を送信しました。</string>
</resources>

View File

@ -310,7 +310,10 @@ class UiAllScreensSanityTest {
clickOn(R.id.createChatRoomButton)
withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
assertDisplayed(R.id.addByMatrixId)
onView(withId(R.id.userListRecyclerView))
.perform(waitForView(withText(R.string.qr_code)))
onView(withId(R.id.userListRecyclerView))
.perform(waitForView(withText(R.string.invite_friends)))
}
closeSoftKeyboard()

View File

@ -72,7 +72,7 @@
android:id="@+id/debug_qr_code"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@tools:sample/avatars" />
tools:src="@drawable/ic_qr_code_add" />
</LinearLayout>

View File

@ -81,7 +81,8 @@
android:resource="@xml/shortcuts" />
</activity-alias>
<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.home.HomeActivity"
android:launchMode="singleTask"/>
<activity
android:name=".features.login.LoginActivity"
android:launchMode="singleTask"
@ -189,10 +190,9 @@
<activity
android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.permalink.PermalinkHandlerActivity">
<activity android:name=".features.permalink.PermalinkHandlerActivity" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -229,6 +229,7 @@
<activity android:name=".features.widgets.WidgetActivity" />
<activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.home.room.detail.search.SearchActivity" />
<activity android:name=".features.usercode.UserCodeActivity" />
<!-- Services -->

View File

@ -111,8 +111,8 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.terms.ReviewTermsFragment
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.usercode.ShowUserCodeFragment
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.widgets.WidgetFragment
@Module
@ -255,13 +255,8 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(UserDirectoryFragment::class)
fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment
@Binds
@IntoMap
@FragmentKey(KnownUsersFragment::class)
fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment
@FragmentKey(UserListFragment::class)
fun bindUserListFragment(fragment: UserListFragment): Fragment
@Binds
@IntoMap
@ -582,4 +577,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SearchFragment::class)
fun bindSearchFragment(fragment: SearchFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ShowUserCodeFragment::class)
fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment
}

View File

@ -50,6 +50,7 @@ import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.link.LinkHandlerActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.VectorAttachmentViewerActivity
import im.vector.app.features.navigation.Navigator
@ -72,6 +73,7 @@ import im.vector.app.features.share.IncomingShareActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.terms.ReviewTermsActivity
import im.vector.app.features.ui.UiStateRepository
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment
@ -140,6 +142,7 @@ interface ScreenComponent {
fun inject(activity: VectorAttachmentViewerActivity)
fun inject(activity: VectorJitsiActivity)
fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity)
/* ==========================================================================================
* BottomSheets
@ -158,6 +161,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet)
fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
fun inject(bottomSheet: MatrixToBottomSheet)
/* ==========================================================================================
* Others

View File

@ -35,7 +35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.reactions.EmojiChooserViewModel
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@Module
interface ViewModelModule {
@ -87,8 +87,8 @@ interface ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(UserDirectorySharedActionViewModel::class)
fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel
@ViewModelKey(UserListSharedActionViewModel::class)
fun bindUserListSharedActionViewModel(viewModel: UserListSharedActionViewModel): ViewModel
@Binds
@IntoMap

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 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.app.core.epoxy
import android.widget.CompoundButton
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.checkbox.MaterialCheckBox
import im.vector.app.R
@EpoxyModelClass(layout = R.layout.item_checkbox)
abstract class CheckBoxItem : VectorEpoxyModel<CheckBoxItem.Holder>() {
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute lateinit var title: String
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.checkbox.isChecked = checked
holder.checkbox.text = title
holder.checkbox.setOnCheckedChangeListener(checkChangeListener)
}
class Holder : VectorEpoxyHolder() {
val checkbox by bind<MaterialCheckBox>(R.id.checkbox)
}
}

View File

@ -26,7 +26,7 @@ import androidx.annotation.DrawableRes
import im.vector.app.R
import im.vector.app.core.platform.SimpleTextWatcher
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_search,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_gray) {
addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {

View File

@ -587,6 +587,16 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
}
fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) {
coordinatorLayout?.let {
Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply {
withActionTitle?.let {
setAction(withActionTitle, { action?.invoke() })
}
}.show()
}
}
/* ==========================================================================================
* User Consent
* ========================================================================================== */

View File

@ -29,6 +29,7 @@ import android.os.Build
import android.os.Environment
import android.provider.Browser
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@ -448,6 +449,19 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
fun openAppSettingsPage(activity: Activity) {
try {
activity.startActivity(
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.fromParts("package", activity.packageName, null)
})
} catch (activityNotFoundException: ActivityNotFoundException) {
activity.toast(R.string.error_no_external_application_found)
}
}
/**
* Ask the user to select a location and a file name to write in
*/

View File

@ -30,6 +30,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import timber.log.Timber
// Android M permission request code management
@ -284,6 +285,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
return isPermissionGranted
}
fun VectorBaseActivity.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) {
showSnackbar(getString(rationaleMessage), R.string.settings) {
openAppSettingsPage(this)
}
}
/**
* Helper method used in [.checkPermissions] to populate the list of the
* permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut).

View File

@ -136,13 +136,19 @@ fun startSharePlainTextIntent(fragment: Fragment,
activityResultLauncher: ActivityResultLauncher<Intent>?,
chooserTitle: String?,
text: String,
subject: String? = null) {
subject: String? = null,
extraTitle: String? = null) {
val share = Intent(Intent.ACTION_SEND)
share.type = "text/plain"
share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
// Add data to the intent, the receiving app will decide what to do with it.
share.putExtra(Intent.EXTRA_SUBJECT, subject)
share.putExtra(Intent.EXTRA_TEXT, text)
extraTitle?.let {
share.putExtra(Intent.EXTRA_TITLE, it)
}
val intent = Intent.createChooser(share, chooserTitle)
try {
if (activityResultLauncher != null) {

View File

@ -30,10 +30,10 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.UserDirectoryAction
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListAction
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import kotlinx.android.synthetic.main.fragment_contacts_book.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
@ -46,16 +46,16 @@ class ContactsBookFragment @Inject constructor(
) : VectorBaseFragment(), ContactsBookController.Callback {
override fun getLayoutResId() = R.layout.fragment_contacts_book
private val viewModel: UserDirectoryViewModel by activityViewModel()
private val viewModel: UserListViewModel by activityViewModel()
// Use activityViewModel to avoid loading several times the data
private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
setupRecyclerView()
setupFilterView()
setupConsentView()
@ -110,7 +110,7 @@ class ContactsBookFragment @Inject constructor(
private fun setupCloseView() {
phoneBookClose.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}
@ -122,13 +122,13 @@ class ContactsBookFragment @Inject constructor(
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}

View File

@ -37,28 +37,31 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import java.net.HttpURLConnection
import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() {
class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
private val viewModel: CreateDirectRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
@ -68,31 +71,34 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
injector.inject(this)
}
override fun create(initialState: UserListViewState): UserListViewModel {
return userListViewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
.subscribe { action ->
when (action) {
UserListSharedAction.Close -> finish()
UserListSharedAction.GoBack -> onBackPressed()
is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action)
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
UserListSharedAction.AddByQrCode -> openAddByQrCode()
}.exhaustive
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
UserListFragment::class.java,
UserListFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
isCreatingRoom = true
menuResId = R.menu.vector_create_direct_room
)
)
}
@ -101,6 +107,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
}
private fun openAddByQrCode() {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
}
private fun openPhoneBook() {
// Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
@ -116,15 +128,23 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
} else {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
} else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact)
}
}
}
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) {
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
action.invitees,
action.existingDmRoomId
null
))
}
}
@ -178,6 +198,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
}

View File

@ -0,0 +1,122 @@
/*
* 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.app.features.createdirect
import android.widget.Toast
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.features.userdirectory.PendingInvitee
import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler {
private val viewModel: CreateDirectRoomViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
startCamera()
}
}
private fun startCamera() {
// Start camera on resume
scannerView.startCamera()
}
override fun onResume() {
super.onResume()
view?.hideKeyboard()
// Register ourselves as a handler for scan results.
scannerView.setResultHandler(this)
// Start camera on resume
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
// Unregister ourselves as a handler for scan results.
scannerView.setResultHandler(null)
// Stop camera on pause
scannerView.stopCamera()
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
private fun addByQrCode(value: String) {
val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId
if (mxid === null) {
Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid)
// The following assumes MXIDs are case insensitive
if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
// Try to get user from known users and fall back to creating a User object from MXID
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle(
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
)
}
}
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
addByQrCode(value)
}
}
}

View File

@ -38,7 +38,7 @@ import org.matrix.android.sdk.rx.rx
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val rawService: RawService,
private val session: Session)
val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedInject.Factory

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
@ -38,8 +39,12 @@ import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.toast
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert
@ -50,10 +55,12 @@ import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import im.vector.app.push.fcm.FcmHelper
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.matrix.android.sdk.api.session.InitialSyncProgressService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
import javax.inject.Inject
@ -64,7 +71,8 @@ data class HomeActivityArgs(
val accountCreation: Boolean
) : Parcelable
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory,
NavigationInterceptor {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -82,6 +90,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler
@Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory
@Inject lateinit var permalinkHandler: PermalinkHandler
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
@ -145,6 +154,28 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
shortcutsHandler.observeRoomsAndBuildShortcuts()
.disposeOnDestroy()
if (isFirstCreation()) {
handleIntent(intent)
}
}
private fun handleIntent(intent: Intent?) {
intent?.dataString?.let { deepLink ->
if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let
permalinkHandler.launch(this, deepLink,
navigationInterceptor = this,
buildTask = true)
// .delay(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled ->
if (!isHandled) {
toast(R.string.permalink_malformed)
}
}
.disposeOnDestroy()
}
}
private fun renderState(state: HomeActivityViewState) {
@ -270,6 +301,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
notificationDrawerManager.clearAllEvents()
}
handleIntent(intent)
}
override fun onDestroy() {
@ -313,11 +345,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
bugReporter.openBugReportScreen(this, false)
return true
}
R.id.menu_home_filter -> {
R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this)
return true
}
R.id.menu_home_setting -> {
R.id.menu_home_setting -> {
navigator.openSettings(this)
return true
}
@ -334,6 +366,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
}
}
override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
val listener = object : MatrixToBottomSheet.InteractionListener {
override fun navigateToRoom(roomId: String) {
navigator.openRoom(this@HomeActivity, roomId)
}
}
// TODO check if there is already one??
MatrixToBottomSheet.withLink(deepLink.toString(), listener)
.show(supportFragmentManager, "HA#MatrixToBottomSheet")
return true
}
companion object {
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
val args = HomeActivityArgs(

View File

@ -18,15 +18,19 @@ package im.vector.app.features.home
import android.os.Bundle
import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.grouplist.GroupListFragment
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.fragment_home_drawer.*
import org.matrix.android.sdk.api.session.Session
@ -75,6 +79,32 @@ class HomeDrawerFragment @Inject constructor(
SignOutUiWorker(requireActivity()).perform()
}
homeDrawerQRCodeButton.debouncedClicks {
UserCodeActivity.newIntent(requireContext(), sharedActionViewModel.session.myUserId).let {
val options =
ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
homeDrawerHeaderAvatarView,
ViewCompat.getTransitionName(homeDrawerHeaderAvatarView) ?: ""
)
startActivity(it, options.toBundle())
}
}
homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
extraTitle = getString(R.string.invite_friends_rich_title)
)
}
}
// Debug menu
homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
homeDrawerHeaderDebugView.debouncedClicks {

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home
import im.vector.app.core.platform.VectorSharedActionViewModel
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()
class HomeSharedActionViewModel @Inject constructor(val session: Session) : VectorSharedActionViewModel<HomeActivitySharedAction>()

View File

@ -1460,7 +1460,7 @@ class RoomDetailFragment @Inject constructor(
return false
}
override fun navToMemberProfile(userId: String): Boolean {
override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
openRoomMemberProfile(userId)
return true
}

View File

@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
@EpoxyModelClass(layout = R.layout.item_room_filter_footer)
abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.Holder>() {
@ -46,7 +46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
val openRoomDirectory by bind<Button>(R.id.roomFilterFooterOpenRoomDirectory)
}
interface FilteredRoomFooterItemListener : FabMenuView.Listener {
interface FilteredRoomFooterItemListener : NotifsFabMenuView.Listener {
fun createRoom(initialName: String)
}
}

View File

@ -45,7 +45,7 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
@ -66,8 +66,7 @@ class RoomListFragment @Inject constructor(
val roomListViewModelFactory: RoomListViewModel.Factory,
private val notificationDrawerManager: NotificationDrawerManager,
private val sharedViewPool: RecyclerView.RecycledViewPool
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, NotifsFabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel

View File

@ -22,15 +22,15 @@ import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.app.R
import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.*
import kotlinx.android.synthetic.main.motion_notifs_fab_menu_merge.view.*
class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
class NotifsFabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
var listener: Listener? = null
init {
inflate(context, R.layout.motion_fab_menu_merge, this)
inflate(context, R.layout.motion_notifs_fab_menu_merge, this)
}
override fun onFinishInflate() {

View File

@ -28,7 +28,7 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserListFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -50,7 +50,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor(
companion object : MvRxViewModelFactory<HomeServerCapabilitiesViewModel, HomeServerCapabilitiesViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel? {
val fragment: KnownUsersFragment = (viewModelContext as FragmentViewModelContext).fragment()
val fragment: UserListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.homeServerCapabilitiesViewModelFactory.create(state)
}

View File

@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
@ -29,7 +30,6 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
@ -39,12 +39,12 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.toast
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure
@ -54,11 +54,11 @@ import javax.inject.Inject
@Parcelize
data class InviteUsersToRoomArgs(val roomId: String) : Parcelable
class InviteUsersToRoomActivity : SimpleFragmentActivity() {
class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
private val viewModel: InviteUsersToRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
@ -68,32 +68,40 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
injector.inject(this)
}
override fun create(initialState: UserListViewState): UserListViewModel {
return userListViewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
}.exhaustive
UserListSharedAction.Close -> finish()
UserListSharedAction.GoBack -> onBackPressed()
is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
// not exhaustive because it's a sharedAction
else -> {
}
}
}
.disposeOnDestroy()
if (isFirstCreation()) {
val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
UserListFragment::class.java,
UserListFragmentArgs(
title = getString(R.string.invite_users_to_room_title),
menuResId = R.menu.vector_invite_users_to_room,
excludedUserIds = viewModel.getUserIdsOfRoomMembers()
excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
existingRoomId = args?.roomId
)
)
}
@ -101,6 +109,12 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
viewModel.observeViewEvents { renderInviteEvents(it) }
}
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
}
}
private fun openPhoneBook() {
// Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
@ -117,12 +131,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
}
}
}
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 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.app.features.matrixto
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.util.MatrixItem
sealed class MatrixToAction : VectorViewModelAction {
data class StartChattingWithUser(val matrixItem: MatrixItem) : MatrixToAction()
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 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.app.features.matrixto
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.*
import javax.inject.Inject
class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class MatrixToArgs(
val matrixToLink: String
) : Parcelable
@Inject lateinit var avatarRenderer: AvatarRenderer
@Inject
lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
private var interactionListener: InteractionListener? = null
override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card
private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class)
interface InteractionListener {
fun navigateToRoom(roomId: String)
}
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
when (val item = state.matrixItem) {
Uninitialized -> {
matrixToCardContentLoading.isVisible = false
matrixToCardUserContentVisibility.isVisible = false
}
is Loading -> {
matrixToCardContentLoading.isVisible = true
matrixToCardUserContentVisibility.isVisible = false
}
is Success -> {
matrixToCardContentLoading.isVisible = false
matrixToCardUserContentVisibility.isVisible = true
matrixToCardNameText.setTextOrHide(item.invoke().displayName)
matrixToCardUserIdText.setTextOrHide(item.invoke().id)
avatarRenderer.render(item.invoke(), matrixToCardAvatar)
}
is Fail -> {
// TODO display some error copy?
dismiss()
}
}
when (state.startChattingState) {
Uninitialized -> {
matrixToCardButtonLoading.isVisible = false
matrixToCardSendMessageButton.isVisible = false
}
is Success -> {
matrixToCardButtonLoading.isVisible = false
matrixToCardSendMessageButton.isVisible = true
}
is Fail -> {
matrixToCardButtonLoading.isVisible = false
matrixToCardSendMessageButton.isVisible = true
// TODO display some error copy?
dismiss()
}
is Loading -> {
matrixToCardButtonLoading.isVisible = true
matrixToCardSendMessageButton.isInvisible = true
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
matrixToCardSendMessageButton.debouncedClicks {
withState(viewModel) {
it.matrixItem.invoke()?.let { item ->
viewModel.handle(MatrixToAction.StartChattingWithUser(item))
}
}
}
viewModel.observeViewEvents {
when (it) {
is MatrixToViewEvents.NavigateToRoom -> {
interactionListener?.navigateToRoom(it.roomId)
dismiss()
}
MatrixToViewEvents.Dismiss -> dismiss()
}
}
}
companion object {
fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet {
return MatrixToBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs(
matrixToLink = matrixToLink
))
}
interactionListener = listener
}
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 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.app.features.matrixto
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.util.MatrixItem
data class MatrixToBottomSheetState(
val deepLink: String,
val matrixItem: Async<MatrixItem> = Uninitialized,
val startChattingState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: MatrixToBottomSheet.MatrixToArgs) : this(
deepLink = args.matrixToLink
)
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) 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.app.features.matrixto
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback
class MatrixToBottomSheetViewModel @AssistedInject constructor(
@Assisted initialState: MatrixToBottomSheetState,
private val session: Session,
private val stringProvider: StringProvider,
private val rawService: RawService) : VectorViewModel<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel
}
init {
setState {
copy(matrixItem = Loading())
}
viewModelScope.launch(Dispatchers.IO) {
resolveLink(initialState)
}
}
private suspend fun resolveLink(initialState: MatrixToBottomSheetState) {
val permalinkData = PermalinkParser.parse(initialState.deepLink)
if (permalinkData is PermalinkData.FallbackLink) {
setState {
copy(
matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.permalink_malformed))),
startChattingState = Uninitialized
)
}
return
}
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = resolveUser(permalinkData.userId)
setState {
copy(
matrixItem = Success(user.toMatrixItem()),
startChattingState = Success(Unit)
)
}
}
is PermalinkData.RoomLink -> {
// not yet supported
_viewEvents.post(MatrixToViewEvents.Dismiss)
}
is PermalinkData.GroupLink -> {
// not yet supported
_viewEvents.post(MatrixToViewEvents.Dismiss)
}
is PermalinkData.FallbackLink -> {
_viewEvents.post(MatrixToViewEvents.Dismiss)
}
}
}
private suspend fun resolveUser(userId: String): User {
return tryOrNull {
awaitCallback<User> {
session.resolveUser(userId, it)
}
}
// Create raw user in case the user is not searchable
?: User(userId, null, null)
}
companion object : MvRxViewModelFactory<MatrixToBottomSheetViewModel, MatrixToBottomSheetState> {
override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? {
val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.matrixToBottomSheetViewModelFactory.create(state)
}
}
override fun handle(action: MatrixToAction) {
when (action) {
is MatrixToAction.StartChattingWithUser -> handleStartChatting(action)
}.exhaustive
}
private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) {
val mxId = action.matrixItem.id
val existing = session.getExistingDirectRoomWithUser(mxId)
if (existing != null) {
// navigate to this room
_viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing))
} else {
setState {
copy(startChattingState = Loading())
}
// we should create the room then navigate
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams()
.apply {
invitedUserIds.add(mxId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
val roomId = try {
awaitCallback<String> { session.createRoom(roomParams, it) }
} catch (failure: Throwable) {
setState {
copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
}
return@launch
}
setState {
// we can hide this button has we will navigate out
copy(startChattingState = Uninitialized)
}
_viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
}
}
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 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.app.features.matrixto
import im.vector.app.core.platform.VectorViewEvents
sealed class MatrixToViewEvents : VectorViewEvents {
data class NavigateToRoom(val roomId: String) : MatrixToViewEvents()
object Dismiss : MatrixToViewEvents()
}

View File

@ -63,13 +63,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.flatMap { permalinkData ->
handlePermalink(permalinkData, context, navigationInterceptor, buildTask)
handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask)
}
.onErrorReturnItem(false)
}
private fun handlePermalink(
permalinkData: PermalinkData,
rawLink: Uri,
context: Context,
navigationInterceptor: NavigationInterceptor?,
buildTask: Boolean
@ -96,7 +97,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
Single.just(true)
}
is PermalinkData.UserLink -> {
if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) {
if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) {
navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
}
Single.just(true)
@ -175,7 +176,7 @@ interface NavigationInterceptor {
/**
* Return true if the navigation has been intercepted
*/
fun navToMemberProfile(userId: String): Boolean {
fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
return false
}
}

View File

@ -23,11 +23,9 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.LoadingFragment
import im.vector.app.features.login.LoginActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class PermalinkHandlerActivity : VectorBaseActivity() {
@ -45,23 +43,28 @@ class PermalinkHandlerActivity : VectorBaseActivity() {
if (isFirstCreation()) {
replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java)
}
handleIntent()
}
private fun handleIntent() {
// If we are not logged in, open login screen.
// In the future, we might want to relaunch the process after login.
if (!sessionHolder.hasActiveSession()) {
startLoginActivity()
return
}
val uri = intent.dataString
permalinkHandler.launch(this, uri, buildTask = true)
.delay(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled ->
if (!isHandled) {
toast(R.string.permalink_malformed)
}
finish()
}
.disposeOnDestroy()
// We forward intent to HomeActivity (singleTask) to avoid the dueling app problem
// https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances
intent.setClass(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
finish()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent()
}
private fun startLoginActivity() {

View File

@ -204,9 +204,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
val viaServers = state.roomDirectoryData.homeServer?.let {
listOf(it)
} ?: emptyList()
val viaServers = state.roomDirectoryData.homeServer
?.let { listOf(it) }
.orEmpty()
session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.

View File

@ -62,7 +62,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
holder.avatarView.isInvisible = directoryAvatarUrl.isNullOrBlank() && includeAllNetworks
holder.nameView.text = directoryName
holder.descritionView.setTextOrHide(directoryDescription)
holder.descriptionView.setTextOrHide(directoryDescription)
}
class Holder : VectorEpoxyHolder() {
@ -70,6 +70,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
val descritionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
val descriptionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
}
}

View File

@ -79,6 +79,17 @@ class RoomMemberProfileController @Inject constructor(
divider = false,
action = { callback?.onIgnoreClicked() }
)
if (!state.isMine) {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "direct",
editable = false,
title = stringProvider.getString(R.string.room_member_open_or_create_dm),
dividerColor = dividerColor,
action = { callback?.onOpenDmClicked() }
)
}
}
private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {

View File

@ -294,12 +294,20 @@ class RoomMemberProfileFragment @Inject constructor(
}
private fun handleShareRoomMemberProfile(permalink: String) {
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = null,
text = permalink
)
val view = layoutInflater.inflate(R.layout.dialog_share_qr_code, null)
val qrCode = view.findViewById<im.vector.app.core.ui.views.QrCodeImageView>(R.id.itemShareQrCodeImage)
qrCode.setData(permalink)
AlertDialog.Builder(requireContext())
.setView(view)
.setNeutralButton(R.string.ok, null)
.setPositiveButton(R.string.share_by_text) { _, _ ->
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = null,
text = permalink
)
}.show()
}
private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) {

View File

@ -16,15 +16,13 @@
package im.vector.app.features.settings
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.preference.Preference
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.displayInWebView
import im.vector.app.core.utils.openAppSettingsPage
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.version.VersionProvider
import im.vector.app.openOssLicensesMenuActivity
@ -42,18 +40,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
// preference to start the App info screen, to facilitate App permissions access
findPreference<VectorPreference>(APP_INFO_LINK_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.let {
val intent = Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val uri = Uri.fromParts("package", requireContext().packageName, null)
data = uri
}
it.applicationContext.startActivity(intent)
}
activity?.let { openAppSettingsPage(it) }
true
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 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.app.features.usercode
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
// Some helper code from BinaryEye
object QRCodeBitmapDecodeHelper {
private val multiFormatReader = MultiFormatReader()
private val decoderHints = mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))
fun decodeQRFromBitmap(bitmap: Bitmap): Result? =
decode(bitmap, false) ?: decode(bitmap, true)
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
private fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
return decodeLuminanceSource(
RGBLuminanceSource(width, height, pixels),
invert
)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decode(bitmap, decoderHints)
} catch (e: ReaderException) {
null
} finally {
multiFormatReader.reset()
}
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 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.app.features.usercode
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import kotlinx.android.synthetic.main.fragment_qr_code_scanner_with_button.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
class ScanUserCodeFragment @Inject constructor()
: VectorBaseFragment(),
ZXingScannerView.ResultHandler {
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner_with_button
val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
var autoFocus = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
userCodeMyCodeButton.debouncedClicks {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
userCodeOpenGalleryButton.debouncedClicks {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
}
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
startCamera()
} else {
// For now just go back
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireActivity(), activityResult.data)
.firstOrNull()
?.contentUri
?.let { uri ->
// try to see if it is a valid matrix code
val bitmap = ImageUtils.getBitmap(requireContext(), uri)
?: return@let Unit.also {
Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show()
}
handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) })
}
}
}
private fun startCamera() {
userCodeScannerView.startCamera()
userCodeScannerView.setAutoFocus(autoFocus)
userCodeScannerView.debouncedClicks {
this.autoFocus = !autoFocus
userCodeScannerView.setAutoFocus(autoFocus)
}
}
override fun onStart() {
super.onStart()
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
userCodeScannerView.setResultHandler(this)
if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
userCodeScannerView.setResultHandler(null)
// Stop camera on pause
userCodeScannerView.stopCamera()
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
sharedViewModel.handle(UserCodeActions.DecodedQRCode(value))
}
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 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.app.features.usercode
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.fragment_user_code_show.*
import javax.inject.Inject
class ShowUserCodeFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_user_code_show
val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
doOpenQRCodeScanner()
} else {
sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showUserCodeClose.debouncedClicks {
sharedViewModel.handle(UserCodeActions.DismissAction)
}
showUserCodeScanButton.debouncedClicks {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
doOpenQRCodeScanner()
}
}
showUserCodeShareButton.debouncedClicks {
sharedViewModel.handle(UserCodeActions.ShareByText)
}
sharedViewModel.observeViewEvents {
if (it is UserCodeShareViewEvents.SharePlainText) {
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = it.title,
text = it.text,
extraTitle = it.richPlainText
)
}
}
}
private fun doOpenQRCodeScanner() {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SCAN))
}
override fun invalidate() = withState(sharedViewModel) { state ->
state.matrixItem?.let { avatarRenderer.render(it, showUserCodeAvatar) }
state.shareLink?.let { showUserCodeQRImage.setData(it) }
showUserCodeCardNameText.setTextOrHide(state.matrixItem?.displayName)
showUserCodeCardUserIdText.setTextOrHide(state.matrixItem?.id)
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 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.app.features.usercode
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.util.MatrixItem
sealed class UserCodeActions : VectorViewModelAction {
object DismissAction : UserCodeActions()
data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions()
data class DecodedQRCode(val code: String) : UserCodeActions()
data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions()
object CameraPermissionNotGranted : UserCodeActions()
object ShareByText : UserCodeActions()
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (c) 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.app.features.usercode
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.matrixto.MatrixToBottomSheet
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_simple.*
import javax.inject.Inject
import kotlin.reflect.KClass
class UserCodeActivity
: VectorBaseActivity(), UserCodeSharedViewModel.Factory, MatrixToBottomSheet.InteractionListener {
@Inject lateinit var viewModelFactory: UserCodeSharedViewModel.Factory
val sharedViewModel: UserCodeSharedViewModel by viewModel()
@Parcelize
data class Args(
val userId: String
) : Parcelable
override fun getLayoutRes() = R.layout.activity_simple
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
// should be there early for shared element transition
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
}
sharedViewModel.selectSubscribe(this, UserCodeState::mode) { mode ->
when (mode) {
UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
is UserCodeState.Mode.RESULT -> {
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet")
}
}
}
sharedViewModel.observeViewEvents {
when (it) {
UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this)
UserCodeShareViewEvents.ShowWaitingScreen -> simpleActivityWaitingView.isVisible = true
UserCodeShareViewEvents.HideWaitingScreen -> simpleActivityWaitingView.isVisible = false
is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId)
UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
else -> {
}
}
}
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
supportFragmentManager.commitTransaction {
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
replace(R.id.simpleFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
}
override fun navigateToRoom(roomId: String) {
navigator.openRoom(this, roomId)
}
override fun onBackPressed() = withState(sharedViewModel) {
when (it.mode) {
UserCodeState.Mode.SHOW -> super.onBackPressed()
is UserCodeState.Mode.RESULT,
UserCodeState.Mode.SCAN -> sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}.exhaustive
}
override fun create(initialState: UserCodeState) =
viewModelFactory.create(initialState)
companion object {
fun newIntent(context: Context, userId: String): Intent {
return Intent(context, UserCodeActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(userId))
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 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.app.features.usercode
import im.vector.app.core.platform.VectorViewEvents
sealed class UserCodeShareViewEvents : VectorViewEvents {
object Dismiss : UserCodeShareViewEvents()
object ShowWaitingScreen : UserCodeShareViewEvents()
object HideWaitingScreen : UserCodeShareViewEvents()
data class ToastMessage(val message: String) : UserCodeShareViewEvents()
data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents()
object CameraPermissionNotGranted : UserCodeShareViewEvents()
data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents()
}

View File

@ -0,0 +1,174 @@
/*
* Copyright (c) 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.app.features.usercode
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback
class UserCodeSharedViewModel @AssistedInject constructor(
@Assisted val initialState: UserCodeState,
private val session: Session,
private val stringProvider: StringProvider,
private val rawService: RawService) : VectorViewModel<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) {
companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> {
override fun create(viewModelContext: ViewModelContext, state: UserCodeState): UserCodeSharedViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
val user = session.getUser(initialState.userId)
setState {
copy(
matrixItem = user?.toMatrixItem(),
shareLink = session.permalinkService().createPermalink(initialState.userId)
)
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserCodeState): UserCodeSharedViewModel
}
override fun handle(action: UserCodeActions) {
when (action) {
UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) }
is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action)
is UserCodeActions.StartChattingWithUser -> handleStartChatting(action)
UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted)
UserCodeActions.ShareByText -> handleShareByText()
}
}
private fun handleShareByText() {
session.permalinkService().createPermalink(session.myUserId)?.let { permalink ->
val text = stringProvider.getString(R.string.invite_friends_text, permalink)
_viewEvents.post(UserCodeShareViewEvents.SharePlainText(
text,
stringProvider.getString(R.string.invite_friends),
stringProvider.getString(R.string.invite_friends_rich_title)
))
}
}
private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) {
val mxId = withUser.matrixItem.id
val existing = session.getExistingDirectRoomWithUser(mxId)
setState {
copy(mode = UserCodeState.Mode.SHOW)
}
if (existing != null) {
// navigate to this room
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing))
} else {
// we should create the room then navigate
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams()
.apply {
invitedUserIds.add(mxId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
val roomId =
try {
awaitCallback<String> { session.createRoom(roomParams, it) }
} catch (failure: Throwable) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
return@launch
} finally {
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
}
}
}
private fun handleQrCodeDecoded(action: UserCodeActions.DecodedQRCode) {
val linkedId = PermalinkParser.parse(action.code)
if (linkedId is PermalinkData.FallbackLink) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_a_valid_qr_code)))
return
}
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
when (linkedId) {
is PermalinkData.RoomLink -> {
// not yet supported
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
}
is PermalinkData.UserLink -> {
val user = tryOrNull {
awaitCallback<User> {
session.resolveUser(linkedId.userId, it)
}
}
// Create raw Uxid in case the user is not searchable
?: User(linkedId.userId, null, null)
setState {
copy(
mode = UserCodeState.Mode.RESULT(user.toMatrixItem(), action.code)
)
}
}
is PermalinkData.GroupLink -> {
// not yet supported
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
}
is PermalinkData.FallbackLink -> {
// not yet supported
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
}
}
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 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.app.features.usercode
import com.airbnb.mvrx.MvRxState
import org.matrix.android.sdk.api.util.MatrixItem
data class UserCodeState(
val userId: String,
val matrixItem: MatrixItem? = null,
val shareLink: String? = null,
val mode: Mode = Mode.SHOW
) : MvRxState {
sealed class Mode {
object SHOW : Mode()
object SCAN : Mode()
data class RESULT(val matrixItem: MatrixItem, val rawLink: String) : Mode()
}
constructor(args: UserCodeActivity.Args) : this(
userId = args.userId
)
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.utils.DebouncedClickListener
@EpoxyModelClass(layout = R.layout.item_contact_action)
abstract class ActionItem : VectorEpoxyModel<ActionItem.Holder>() {
@EpoxyAttribute var title: CharSequence? = null
@EpoxyAttribute @DrawableRes var actionIconRes: Int? = null
@EpoxyAttribute var clickAction: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener(clickAction?.let { DebouncedClickListener(it) })
// If name is empty, use userId as name and force it being centered
holder.actionTitleText.setTextOrHide(title)
if (actionIconRes != null) {
holder.actionTitleImageView.setImageResource(actionIconRes!!)
} else {
holder.actionTitleImageView.setImageDrawable(null)
}
}
class Holder : VectorEpoxyHolder() {
val actionTitleText by bind<TextView>(R.id.actionTitleText)
val actionTitleImageView by bind<ImageView>(R.id.actionIconImageView)
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_contact_detail)
abstract class ContactDetailItem : VectorEpoxyModel<ContactDetailItem.Holder>() {
@EpoxyAttribute lateinit var threePid: String
@EpoxyAttribute var matrixId: String? = null
@EpoxyAttribute var clickListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.onClick(clickListener)
holder.nameView.text = threePid
holder.matrixIdView.setTextOrHide(matrixId)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.contactDetailName)
val matrixIdView by bind<TextView>(R.id.contactDetailMatrixId)
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_contact_main)
abstract class ContactItem : VectorEpoxyModel<ContactItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var mappedContact: MappedContact
override fun bind(holder: Holder) {
super.bind(holder)
// If name is empty, use userId as name and force it being centered
holder.nameView.text = mappedContact.displayName
avatarRenderer.render(mappedContact, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.contactDisplayName)
val avatarImageView by bind<ImageView>(R.id.contactAvatar)
}
}

View File

@ -1,139 +0,0 @@
/*
* Copyright (c) 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.app.features.userdirectory
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.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class DirectoryUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: UserDirectoryViewState? = null
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: UserDirectoryViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank()
when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading()
is Success -> renderSuccess(
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
currentState.getSelectedMatrixId(),
hasSearch
)
is Fail -> renderFailure(asyncUsers.error)
}
}
/**
* Eventually add the searched terms, if it is a userId, and if not already present in the result
*/
private fun computeUsersList(directoryUsers: List<User>, searchTerms: String): List<User> {
return directoryUsers +
searchTerms
.takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
?.let { listOf(User(it)) }
.orEmpty()
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
listener { callback?.retryDirectoryUsersRequest() }
}
}
private fun renderSuccess(users: List<User>,
selectedUsers: List<String>,
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers)
}
}
private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
for (user in users) {
if (user.userId == session.myUserId) {
continue
}
val isSelected = selectedUsers.contains(user.userId)
userDirectoryUserItem {
id(user.userId)
selected(isSelected)
matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}
private fun renderEmptyState(hasSearch: Boolean) {
val noResultRes = if (hasSearch) {
R.string.no_result_placeholder
} else {
R.string.direct_room_start_search
}
noResultItem {
id("noResult")
text(stringProvider.getString(noResultRes))
}
}
interface Callback {
fun onItemClick(user: User)
fun retryDirectoryUsersRequest()
}
}

View File

@ -1,122 +0,0 @@
/*
* Copyright (c) 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.app.features.userdirectory
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.EmptyItem_
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class KnownUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider) : PagedListEpoxyController<User>(
modelBuildingHandler = createUIHandler()
) {
private var selectedUsers: List<String> = emptyList()
private var users: Async<List<User>> = Uninitialized
private var isFiltering: Boolean = false
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: UserDirectoryViewState) {
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
val newSelection = state.getSelectedMatrixId()
this.users = state.knownUsers
if (newSelection != selectedUsers) {
this.selectedUsers = newSelection
requestForcedModelBuild()
}
submitList(state.knownUsers())
}
override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
return if (item == null) {
EmptyItem_().id(currentPosition)
} else {
val isSelected = selectedUsers.contains(item.userId)
UserDirectoryUserItem_()
.id(item.userId)
.selected(isSelected)
.matrixItem(item.toMatrixItem())
.avatarRenderer(avatarRenderer)
.clickListener { _ ->
callback?.onItemClick(item)
}
}
}
override fun addModels(models: List<EpoxyModel<*>>) {
if (users is Incomplete) {
renderLoading()
} else if (models.isEmpty()) {
renderEmptyState()
} else {
var lastFirstLetter: String? = null
for (model in models) {
if (model is UserDirectoryUserItem) {
if (model.matrixItem.id == session.myUserId) continue
val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter
UserDirectoryLetterHeaderItem_()
.id(currentFirstLetter)
.letter(currentFirstLetter)
.addIf(showLetter, this)
model.addTo(this)
} else {
continue
}
}
}
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderEmptyState() {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.direct_room_no_known_users))
}
}
interface Callback {
fun onItemClick(user: User)
}
}

View File

@ -1,94 +0,0 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_user_directory.*
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class UserDirectoryFragment @Inject constructor(
private val directRoomController: DirectoryUsersController
) : VectorBaseFragment(), DirectoryUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_user_directory
private val viewModel: UserDirectoryViewModel by activityViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView()
setupSearchByMatrixIdView()
setupCloseView()
}
override fun onDestroyView() {
userDirectoryRecyclerView.cleanup()
directRoomController.callback = null
super.onDestroyView()
}
private fun setupRecyclerView() {
directRoomController.callback = this
userDirectoryRecyclerView.configureWith(directRoomController)
}
private fun setupSearchByMatrixIdView() {
userDirectorySearchById.setupAsSearch(searchIconRes = 0)
userDirectorySearchById
.textChanges()
.subscribe {
viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString()))
}
.disposeOnDestroyView()
userDirectorySearchById.showKeyboard(andRequestFocus = true)
}
private fun setupCloseView() {
userDirectoryClose.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
}
}
override fun invalidate() = withState(viewModel) {
directRoomController.setData(it)
}
override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
}
override fun retryDirectoryUsersRequest() {
val currentSearch = userDirectorySearchById.text.toString()
viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch))
}
}

View File

@ -1,153 +0,0 @@
/*
* Copyright (c) 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.app.features.userdirectory
import androidx.fragment.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.invite.InviteUsersToRoomActivity
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
class UserDirectoryViewModel @AssistedInject constructor(@Assisted
initialState: UserDirectoryViewState,
private val session: Session)
: VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
}
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? {
return when (viewModelContext) {
is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
is ActivityViewModelContext -> {
when (viewModelContext.activity<FragmentActivity>()) {
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().userDirectoryViewModelFactory.create(state)
else -> error("Wrong activity or fragment")
}
}
else -> error("Wrong activity or fragment")
}
}
}
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: UserDirectoryAction) {
when (action) {
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is UserDirectoryAction.SelectPendingInvitee -> handleSelectUser(action)
is UserDirectoryAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
}.exhaustive
}
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
// Reset the filter asap
directoryUsersSearch.accept("")
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun getExistingDmRoomId(selectedUsers: Set<PendingInvitee>): String? {
return selectedUsers
.takeIf { it.size == 1 }
?.filterIsInstance(PendingInvitee.UserPendingInvitee::class.java)
?.firstOrNull()
?.let { invitee -> session.getExistingDirectRoomWithUser(invitee.user.userId) }
}
private fun observeDirectoryUsers() = withState { state ->
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}
private fun observeKnownUsers() = withState { state ->
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull(), state.excludedUserIds)
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}
}

View File

@ -18,10 +18,10 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorViewModelAction
sealed class UserDirectoryAction : VectorViewModelAction {
data class FilterKnownUsers(val value: String) : UserDirectoryAction()
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
object ClearFilterKnownUsers : UserDirectoryAction()
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
sealed class UserListAction : VectorViewModelAction {
data class SearchUsers(val value: String) : UserListAction()
object ClearSearchUsers : UserListAction()
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
object ComputeMatrixToLinkForSharing : UserListAction()
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.view.View
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.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class UserListController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: UserListViewState? = null
var callback: Callback? = null
fun setData(state: UserListViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
// Build generic items
if (currentState.searchTerm.isBlank()) {
// For now we remove this option if in invite to existing room flow (and not create DM)
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
actionItem {
id(R.drawable.ic_share)
title(stringProvider.getString(R.string.invite_friends))
actionIconRes(R.drawable.ic_share)
clickAction(View.OnClickListener {
callback?.onInviteFriendClick()
})
}
}
actionItem {
id(R.drawable.ic_baseline_perm_contact_calendar_24)
title(stringProvider.getString(R.string.contacts_book_title))
actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
clickAction(View.OnClickListener {
callback?.onContactBookClick()
})
}
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
actionItem {
id(R.drawable.ic_qr_code_add)
title(stringProvider.getString(R.string.qr_code))
actionIconRes(R.drawable.ic_qr_code_add)
clickAction(View.OnClickListener {
callback?.onUseQRCode()
})
}
}
}
when (currentState.knownUsers) {
is Uninitialized -> renderEmptyState()
is Loading -> renderLoading()
is Fail -> renderFailure(currentState.knownUsers.error)
is Success -> buildKnownUsers(currentState, currentState.getSelectedMatrixId())
}
when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> {
}
is Loading -> renderLoading()
is Fail -> renderFailure(asyncUsers.error)
is Success -> buildDirectoryUsers(
asyncUsers(),
currentState.getSelectedMatrixId(),
currentState.searchTerm,
// to avoid showing twice same user in known and suggestions
currentState.knownUsers.invoke()?.map { it.userId }.orEmpty()
)
}
}
private fun buildKnownUsers(currentState: UserListViewState, selectedUsers: List<String>) {
currentState.knownUsers()?.let { userList ->
userListHeaderItem {
id("known_header")
header(stringProvider.getString(R.string.direct_room_user_list_known_title))
}
if (userList.isEmpty()) {
renderEmptyState()
return
}
userList.forEach { item ->
val isSelected = selectedUsers.contains(item.userId)
userDirectoryUserItem {
id(item.userId)
selected(isSelected)
matrixItem(item.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(item)
}
}
}
}
}
private fun buildDirectoryUsers(directoryUsers: List<User>, selectedUsers: List<String>, searchTerms: String, ignoreIds: List<String>) {
val toDisplay = directoryUsers.filter { !ignoreIds.contains(it.userId) }
if (toDisplay.isEmpty() && searchTerms.isBlank()) {
return
}
userListHeaderItem {
id("suggestions")
header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title))
}
if (toDisplay.isEmpty()) {
renderEmptyState()
} else {
toDisplay.forEach { user ->
if (user.userId != session.myUserId) {
val isSelected = selectedUsers.contains(user.userId)
userDirectoryUserItem {
id(user.userId)
selected(isSelected)
matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}
}
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderEmptyState() {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
}
private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
}
}
interface Callback {
fun onInviteFriendClick()
fun onContactBookClick()
fun onUseQRCode()
fun onItemClick(user: User)
fun onMatrixIdClick(matrixId: String)
fun onThreePidClick(threePid: ThreePid)
}
}

View File

@ -36,53 +36,64 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import kotlinx.android.synthetic.main.fragment_known_users.*
import kotlinx.android.synthetic.main.fragment_user_list.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class KnownUsersFragment @Inject constructor(
val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory,
private val knownUsersController: KnownUsersController,
class UserListFragment @Inject constructor(
private val userListController: UserListController,
private val dimensionConverter: DimensionConverter,
val homeServerCapabilitiesViewModelFactory: HomeServerCapabilitiesViewModel.Factory
) : VectorBaseFragment(), KnownUsersController.Callback {
) : VectorBaseFragment(), UserListController.Callback {
private val args: KnownUsersFragmentArgs by args()
private val args: UserListFragmentArgs by args()
private val viewModel: UserListViewModel by activityViewModel()
private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
override fun getLayoutResId() = R.layout.fragment_known_users
override fun getLayoutResId() = R.layout.fragment_user_list
override fun getMenuRes() = args.menuResId
private val viewModel: UserDirectoryViewModel by activityViewModel()
private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
userListTitle.text = args.title
vectorBaseActivity.setSupportActionBar(userListToolbar)
knownUsersTitle.text = args.title
vectorBaseActivity.setSupportActionBar(knownUsersToolbar)
setupRecyclerView()
setupFilterView()
setupAddByMatrixIdView()
setupAddFromPhoneBookView()
setupSearchView()
setupCloseView()
homeServerCapabilitiesViewModel.subscribe {
knownUsersE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
}
viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) {
viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) {
renderSelectedUsers(it)
}
viewModel.observeViewEvents {
when (it) {
is UserListViewEvents.OpenShareMatrixToLing -> {
val text = getString(R.string.invite_friends_text, it.link)
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
extraTitle = getString(R.string.invite_friends_rich_title)
)
}
}
}
}
override fun onDestroyView() {
knownUsersController.callback = null
knownUsersRecyclerView.cleanup()
userListRecyclerView.cleanup()
super.onDestroyView()
}
@ -91,69 +102,52 @@ class KnownUsersFragment @Inject constructor(
val showMenuItem = it.pendingInvitees.isNotEmpty()
menu.forEach { menuItem ->
menuItem.isVisible = showMenuItem
if (args.isCreatingRoom) {
menuItem.setTitle(if (it.existingDmRoomId != null) R.string.action_open else R.string.create_room_action_create)
}
}
}
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(
item.itemId,
it.pendingInvitees,
it.existingDmRoomId
))
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
return@withState true
}
private fun setupAddByMatrixIdView() {
addByMatrixId.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory)
}
}
private fun setupAddFromPhoneBookView() {
addFromPhoneBook.debouncedClicks {
// TODO handle Permission first
sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
}
}
private fun setupRecyclerView() {
knownUsersController.callback = this
userListController.callback = this
// Don't activate animation as we might have way to much item animation when filtering
knownUsersRecyclerView.configureWith(knownUsersController, disableItemAnimation = true)
userListRecyclerView.configureWith(userListController, disableItemAnimation = true)
}
private fun setupFilterView() {
knownUsersFilter
private fun setupSearchView() {
withState(viewModel) {
userListSearch.hint = getString(R.string.user_directory_search_hint)
}
userListSearch
.textChanges()
.startWith(knownUsersFilter.text)
.startWith(userListSearch.text)
.subscribe { text ->
val filterValue = text.trim()
val action = if (filterValue.isBlank()) {
UserDirectoryAction.ClearFilterKnownUsers
val searchValue = text.trim()
val action = if (searchValue.isBlank()) {
UserListAction.ClearSearchUsers
} else {
UserDirectoryAction.FilterKnownUsers(filterValue.toString())
UserListAction.SearchUsers(searchValue.toString())
}
viewModel.handle(action)
}
.disposeOnDestroyView()
knownUsersFilter.setupAsSearch()
knownUsersFilter.requestFocus()
userListSearch.setupAsSearch()
userListSearch.requestFocus()
}
private fun setupCloseView() {
knownUsersClose.debouncedClicks {
userListClose.debouncedClicks {
requireActivity().finish()
}
}
override fun invalidate() = withState(viewModel) {
knownUsersController.setData(it)
userListController.setData(it)
}
private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {
@ -183,12 +177,35 @@ class KnownUsersFragment @Inject constructor(
chip.isCloseIconVisible = true
chipGroup.addView(chip)
chip.setOnCloseIconClickListener {
viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee))
viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee))
}
}
override fun onInviteFriendClick() {
viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing)
}
override fun onContactBookClick() {
sharedActionViewModel.post(UserListSharedAction.OpenPhoneBook)
}
override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
}
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
}
override fun onUseQRCode() {
view?.hideKeyboard()
sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
}
}

View File

@ -20,9 +20,9 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class KnownUsersFragmentArgs(
data class UserListFragmentArgs(
val title: String,
val menuResId: Int,
val excludedUserIds: Set<String>? = null,
val isCreatingRoom: Boolean = false
val existingRoomId: String? = null
) : Parcelable

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 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.app.features.userdirectory
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_user_list_header)
abstract class UserListHeaderItem : VectorEpoxyModel<UserListHeaderItem.Holder>() {
@EpoxyAttribute var header: String = ""
override fun bind(holder: Holder) {
super.bind(holder)
holder.headerTextView.text = header
}
class Holder : VectorEpoxyHolder() {
val headerTextView by bind<TextView>(R.id.userListHeaderView)
}
}

View File

@ -18,12 +18,10 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorSharedAction
sealed class UserDirectorySharedAction : VectorSharedAction {
object OpenUsersDirectory : UserDirectorySharedAction()
object OpenPhoneBook : UserDirectorySharedAction()
object Close : UserDirectorySharedAction()
object GoBack : UserDirectorySharedAction()
data class OnMenuItemSelected(val itemId: Int,
val invitees: Set<PendingInvitee>,
val existingDmRoomId: String?) : UserDirectorySharedAction()
sealed class UserListSharedAction : VectorSharedAction {
object Close : UserListSharedAction()
object GoBack : UserListSharedAction()
data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction()
object OpenPhoneBook : UserListSharedAction()
object AddByQrCode : UserListSharedAction()
}

View File

@ -19,4 +19,4 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserDirectorySharedAction>()
class UserListSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserListSharedAction>()

View File

@ -21,4 +21,6 @@ import im.vector.app.core.platform.VectorViewEvents
/**
* Transient events for invite users to room screen
*/
sealed class UserDirectoryViewEvents : VectorViewEvents
sealed class UserListViewEvents : VectorViewEvents {
data class OpenShareMatrixToLing(val link: String) : UserListViewEvents()
}

View File

@ -0,0 +1,180 @@
/*
* Copyright (c) 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.app.features.userdirectory
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
private val session: Session)
: VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private var currentUserSearchDisposable: Disposable? = null
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserListViewState): UserListViewModel
}
companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {
override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
setState {
copy(
myUserId = session.myUserId,
existingRoomId = initialState.existingRoomId
)
}
observeUsers()
}
override fun handle(action: UserListAction) {
when (action) {
is UserListAction.SearchUsers -> handleSearchUsers(action.value)
is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
is UserListAction.SelectPendingInvitee -> handleSelectUser(action)
is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
}.exhaustive
}
private fun handleSearchUsers(searchTerm: String) {
setState {
copy(searchTerm = searchTerm)
}
knownUsersSearch.accept(searchTerm)
directoryUsersSearch.accept(searchTerm)
}
private fun handleShareMyMatrixToLink() {
session.permalinkService().createPermalink(session.myUserId)?.let {
_viewEvents.post(UserListViewEvents.OpenShareMatrixToLing(it))
}
}
private fun handleClearSearchUsers() {
knownUsersSearch.accept("")
directoryUsersSearch.accept("")
setState {
copy(searchTerm = "")
}
}
private fun observeUsers() = withState { state ->
knownUsersSearch
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it, state.excludedUserIds)
}
.execute { async ->
copy(knownUsers = async)
}
currentUserSearchDisposable?.dispose()
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList<User>())
} else {
val searchObservable = session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
// If it's a valid user id try to use Profile API
// because directory only returns users that are in public rooms or share a room with you, where as
// profile will work other federations
if (!MatrixPatterns.isUserId(search)) {
searchObservable
} else {
val profileObservable = session.rx().getProfileInfo(search)
.map { json ->
User(
userId = search,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
).toOptional()
}
.onErrorReturn { Optional.empty() }
Single.zip(searchObservable, profileObservable, { searchResults, optionalProfile ->
val profile = optionalProfile.getOrNull() ?: return@zip searchResults
val searchContainsProfile = searchResults.indexOfFirst { it.userId == profile.userId } != -1
if (searchContainsProfile) {
searchResults
} else {
listOf(profile) + searchResults
}
})
}
}
stream.toAsync {
copy(directoryUsers = it)
}
}
.subscribe()
.disposeOnClear()
}
private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
}

View File

@ -17,30 +17,33 @@
package im.vector.app.features.userdirectory
import androidx.paging.PagedList
import arrow.core.Option
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.contacts.MappedContact
import org.matrix.android.sdk.api.session.user.model.User
data class UserDirectoryViewState(
data class UserListViewState(
val excludedUserIds: Set<String>? = null,
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val filteredMappedContacts: List<MappedContact> = emptyList(),
val pendingInvitees: Set<PendingInvitee> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty(),
val existingDmRoomId: String? = null
val searchTerm: String = "",
val myUserId: String = "",
val existingRoomId: String? = null
) : MvRxState {
constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
constructor(args: UserListFragmentArgs) : this(
existingRoomId = args.existingRoomId
)
fun getSelectedMatrixId(): List<String> {
return pendingInvitees
.mapNotNull {
when (it) {
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.ThreePidPendingInvitee -> null
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,19.5C4,18.1193 5.1193,17 6.5,17H20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6.5,2H20V22H6.5C5.1193,22 4,20.8807 4,19.5V4.5C4,3.1193 5.1193,2 6.5,2Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More