mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 10:25:51 +03:00
Merge branch 'develop' into feature/bca/home_empty_screens
This commit is contained in:
commit
32d42794dd
141 changed files with 4407 additions and 2576 deletions
|
@ -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)
|
||||
|
|
|
@ -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 🗣:
|
||||
-
|
||||
|
|
1
fastlane/metadata/android/ca/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/ca/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// TODO
|
30
fastlane/metadata/android/ca/full_description.txt
Normal file
30
fastlane/metadata/android/ca/full_description.txt
Normal 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.
|
1
fastlane/metadata/android/ca/short_description.txt
Normal file
1
fastlane/metadata/android/ca/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Xat i VoIP segurs i descentralitzats. Protegeix les teves dades de tercers.
|
1
fastlane/metadata/android/ca/title.txt
Normal file
1
fastlane/metadata/android/ca/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Element (anteriorment Riot.im)
|
1
fastlane/metadata/android/de/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/de/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// TODO
|
1
fastlane/metadata/android/es/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/es/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// TODO
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Element (anteriorment Riot.im)
|
||||
Element (previamente Riot.im)
|
||||
|
|
1
fastlane/metadata/android/fa/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/fa/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// برای انجام
|
1
fastlane/metadata/android/it/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/it/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// DA FARE
|
1
fastlane/metadata/android/nb/short_description.txt
Normal file
1
fastlane/metadata/android/nb/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter.
|
1
fastlane/metadata/android/nb/title.txt
Normal file
1
fastlane/metadata/android/nb/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Element (tidligere Riot.im)
|
1
fastlane/metadata/android/pt_BR/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/pt_BR/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// A FAZER
|
1
fastlane/metadata/android/sv/changelogs/40100100.txt
Normal file
1
fastlane/metadata/android/sv/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// ATT GÖRA
|
|
@ -0,0 +1 @@
|
|||
// 待辦事項
|
30
fastlane/metadata/android/zh_Hant/full_description.txt
Normal file
30
fastlane/metadata/android/zh_Hant/full_description.txt
Normal 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 來在所有裝置與網路上保持訊息歷史同步。
|
1
fastlane/metadata/android/zh_Hant/short_description.txt
Normal file
1
fastlane/metadata/android/zh_Hant/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
安全的去中心化聊天與 VoIP。確保您的資料不受第三方的影響。
|
1
fastlane/metadata/android/zh_Hant/title.txt
Normal file
1
fastlane/metadata/android/zh_Hant/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Element(曾名為 Riot.im)
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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>()
|
|
@ -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()
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
21
vector/src/main/res/drawable/ic_book.xml
Normal file
21
vector/src/main/res/drawable/ic_book.xml
Normal 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
Loading…
Reference in a new issue