Merge branch 'develop' into feature/universal_link_navigation

This commit is contained in:
Onuray Sahin 2020-09-07 17:48:52 +03:00 committed by GitHub
commit 6dd4d4d906
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
227 changed files with 10551 additions and 3262 deletions

View file

@ -2,31 +2,44 @@ Changes in Element 1.0.6 (2020-XX-XX)
===================================================
Features ✨:
-
- List phone numbers and emails added to the Matrix account, and add emails and phone numbers to account (#44, #45)
Improvements 🙌:
- You can now join room through permalink and within room directory search
- Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774)
- Fix several issues when uploading bug files (#1889)
- Do not propose to verify session if there is only one session and 4S is not configured (#1901)
- Call screen does not use proximity sensor (#1735)
Bugfix 🐛:
- Display name not shown under Settings/General (#1926)
- Editing message forgets line breaks and markdown (#1939)
- Words containing my name should not trigger notifications (#1781)
- Fix changing language issue
- Fix FontSize issue (#1483, #1787)
- Fix bad color for settings icon on Android < 24 (#1786)
- Change user or room avatar: when selecting Gallery, I'm not proposed to crop the selected image (#1590)
- Loudspeaker is always used (#1685)
- Fix uploads still don't work with room v6 (#1879)
- Can't handle ongoing call events in background (#1992)
- Handle room, user and group links by the Element app (#1795)
- Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034
- Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027)
- Support for image compression on Android 10
- Verification popup won't show
Translations 🗣:
-
- The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909)
SDK API changes ⚠️:
-
Build 🧱:
- Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging)
- Buildkite:
New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml
New build location: https://buildkite.com/matrix-dot-org/element-android
Other changes:
- Use File extension functions to make code more concise (#1996)

View file

@ -1,4 +1,4 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
[![Weblate](https://translate.riot.im/widgets/element-android/-/svg-badge.svg)](https://translate.riot.im/engage/element-android/?utm_source=widget)
[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
# New Android SDK

View file

@ -27,4 +27,9 @@ class AnimatedImageViewHolder constructor(itemView: View) :
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
override fun onRecycled() {
super.onRecycled()
touchImageView.setImageDrawable(null)
}
}

View file

@ -50,6 +50,7 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
@ -77,11 +78,13 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true
holder.touchImageView.setImageDrawable(placeholder)
}
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {

View file

@ -35,6 +35,7 @@ interface VideoLoaderTarget {
fun onVideoFileLoading(uid: String)
fun onVideoFileLoadFailed(uid: String)
fun onVideoFileReady(uid: String, file: File)
fun onVideoURLReady(uid: String, path: String)
}
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
@ -47,6 +48,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
}
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.thumbnailImage.setImageDrawable(placeholder)
}
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
@ -68,9 +71,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
override fun onVideoFileReady(uid: String, file: File) {
if (holder.boundResourceUid != uid) return
arrangeForVideoReady()
holder.videoReady(file)
}
override fun onVideoURLReady(uid: String, path: String) {
if (holder.boundResourceUid != uid) return
arrangeForVideoReady()
holder.videoReady(path)
}
private fun arrangeForVideoReady() {
holder.thumbnailImage.isVisible = false
holder.loaderProgressBar.isVisible = false
holder.videoView.isVisible = true
holder.videoReady(file)
}
}

View file

@ -16,6 +16,7 @@
package im.vector.lib.attachmentviewer
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
@ -65,6 +66,13 @@ class VideoViewHolder constructor(itemView: View) :
}
}
fun videoReady(path: String) {
mVideoPath = path
if (isSelected) {
startPlaying()
}
}
fun videoFileLoadError() {
}
@ -118,8 +126,13 @@ class VideoViewHolder constructor(itemView: View) :
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
}
}
try {
videoView.setVideoPath(mVideoPath)
} catch (failure: Throwable) {
// Couldn't open
Log.v(VideoViewHolder::class.java.name, "Failed to start video")
}
videoView.setVideoPath(mVideoPath)
if (!wasPaused) {
videoView.start()
if (progress > 0) {

View file

@ -39,4 +39,9 @@ class ZoomableImageViewHolder constructor(itemView: View) :
}
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
override fun onRecycled() {
super.onRecycled()
touchImageView.setImageDrawable(null)
}
}

View file

@ -87,7 +87,7 @@ sonarqube {
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8"
property "sonar.links.homepage", "https://github.com/vector-im/element-android/"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android"
property "sonar.links.scm", "https://github.com/vector-im/element-android/"
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization"

285
docs/add_threePids.md Normal file
View file

@ -0,0 +1,285 @@
# Adding and removing ThreePids to an account
## Add email
### User enter the email
> POST https://homeserver.org/_matrix/client/r0/account/3pid/email/requestToken
```json
{
"email": "alice@email-provider.org",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
"send_attempt": 1
}
```
#### The email is already added to an account
400
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "Email is already in use"
}
```
#### The email is free
Wording: "We've sent you an email to verify your address. Please follow the instructions there and then click the button below."
200
```json
{
"sid": "bxyDHuJKsdkjMlTJ"
}
```
## User receive an e-mail
> [homeserver.org] Validate your email
>
> A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email:
https://homeserver.org/_matrix/client/unstable/add_threepid/email/submit_token?token=WUnEhQAmJrXupdEbXgdWvnVIKaGYZFsU&client_secret=TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh&sid=bxyDHuJKsdkjMlTJ
>
> If this was not you, you can safely ignore this email. Thank you.
### User clicks on the link
The browser displays the following message:
> Your email has now been validated, please return to your client. You may now close this window.
### User returns on Element
User clicks on CONTINUE
> POST https://homeserver.org/_matrix/client/r0/account/3pid/add
```json
{
"sid": "bxyDHuJKsdkjMlTJ",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh"
}
```
401 User Interactive Authentication
```json
{
"session": "ppvvnozXCQZFaggUBlHJYPjA",
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {
}
}
```
### User enters his password
POST https://homeserver.org/_matrix/client/r0/account/3pid/add
```json
{
"sid": "bxyDHuJKsdkjMlTJ",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
"auth": {
"session": "ppvvnozXCQZFaggUBlHJYPjA",
"type": "m.login.password",
"user": "@benoitx:matrix.org",
"password": "weak_password"
}
}
```
#### The link has not been clicked
400
```json
{
"errcode": "M_THREEPID_AUTH_FAILED",
"error": "No validated 3pid session found"
}
```
#### Wrong password
401
```json
{
"session": "fXHOvoQsPMhEebVqTnIrzZJN",
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {
},
"completed":[
],
"error": "Invalid password",
"errcode": "M_FORBIDDEN"
}
```
#### The link has been clicked and the account password is correct
200
```json
{}
```
## Remove email
### User want to remove an email from his account
> POST https://homeserver.org/_matrix/client/r0/account/3pid/delete
```json
{
"medium": "email",
"address": "alice@email-provider.org"
}
```
#### Email was not bound to an identity server
200
```json
{
"id_server_unbind_result": "no-support"
}
```
#### Email was bound to an identity server
200
```json
{
"id_server_unbind_result": "success"
}
```
## Add phone number
> POST https://homeserver.org/_matrix/client/r0/account/3pid/msisdn/requestToken
```json
{
"country": "FR",
"phone_number": "611223344",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"send_attempt": 1
}
```
Note that the phone number is sent without `+` and without the country code
#### The phone number is already added to an account
400
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "MSISDN is already in use"
}
```
#### The phone number is free
Wording: "A text message has been sent to +33611223344. Please enter the verification code it contains."
200
```json
{
"msisdn": "33651547677",
"intl_fmt": "+33 6 51 54 76 77",
"success": true,
"sid": "253299954",
"submit_url": "https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token"
}
```
## User receive a text message
> Riot
> Your Riot validation code is 892541, please enter this into the app
### User enter the code to the app
#### Wrong code
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
```json
{
"sid": "253299954",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"token": "111111"
}
```
400
```json
{
"errcode": "M_UNKNOWN",
"error": "Error contacting the identity server"
}
```
This is not an ideal, but the client will display a hint to check the entered code to the user.
#### Correct code
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
```json
{
"sid": "253299954",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"token": "892541"
}
```
200
```json
{
"success": true
}
```
Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow
## Remove phone number
### User wants to remove a phone number from his account
This is the same request and response than to remove email, but with this body:
```json
{
"medium": "msisdn",
"address": "33611223344"
}
```
Note that the phone number is provided without `+`, but with the country code.

View file

@ -8,7 +8,9 @@ This document describes the flow of signin to a homeserver, and also the flow wh
Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
> curl -X GET 'https://matrix.org/_matrix/client/r0/login'
```shell script
curl -X GET 'https://matrix.org/_matrix/client/r0/login'
```
200
@ -26,7 +28,9 @@ Client request the sign-in flows, once the homeserver is chosen by the user and
The user is able to connect using `m.login.password`
> curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```shell script
curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```
```json
{
@ -73,14 +77,16 @@ We get credential (200)
If the user has associated an email with its account, he can signin using the email.
> curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```shell script
curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@email-provider.org"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```
```json
{
"identifier": {
"type": "m.id.thirdparty",
"medium": "email",
"address": "alice@yopmail.com"
"address": "alice@email-provider.org"
},
"password": "weak_password",
"type": "m.login.password",
@ -136,7 +142,9 @@ Not supported yet in Element
### Login with SSO
> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
```shell script
curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
```
200
@ -171,7 +179,9 @@ Once the process is finished, the web page will call the `redirectUrl` with an e
This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```shell script
curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```
```json
{
@ -214,7 +224,9 @@ We display a warning regarding e2e.
At the first step, we do not send the password, only the email and a client secret, generated by the application
> curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
```shell script
curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
```
```json
{
@ -251,7 +263,9 @@ During this step, the new password is sent to the homeserver.
If the user confirms before the link is clicked, we get an error:
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```shell script
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```
```json
{
@ -285,7 +299,9 @@ It contains the client secret, a token and the sid
When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand):
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```shell script
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```
```json
{

View file

@ -10,7 +10,9 @@ This document describes the flow of registration to a homeserver. Examples come
Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
> curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -70,7 +72,9 @@ If the registration is not possible, we get a 403
The app is displaying a form to enter username and password.
> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -133,9 +137,11 @@ We get a 400:
### Step 2: entering email
User is proposed to enter an email. We skip this step.
User is proposed to enter an email. User skips this step.
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -189,16 +195,18 @@ User is proposed to enter an email. We skip this step.
}
```
### Step 2 bis: we enter an email
### Step 2 bis: user enters an email
We request a token to the homeserver. The `client_secret` is generated by the application
> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken'
```shell script
curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@email-provider.org","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken'
```
```json
{
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
"email": "alice@yopmail.com",
"email": "alice@email-provider.org",
"send_attempt": 0
}
```
@ -213,7 +221,9 @@ We request a token to the homeserver. The `client_secret` is generated by the ap
And
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -239,7 +249,9 @@ We get 401 since the email is not validated yet:
The app is now polling on
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -254,7 +266,7 @@ The app is now polling on
}
```
We click on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains:
User clicks on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains:
- A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ
- The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa
- A `sid`: qlBCREDACTEDEtgxD
@ -306,7 +318,9 @@ Once the link is clicked, the registration request (polling) returns a 401 with
User is proposed to accept T&C and he accepts them
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -365,7 +379,9 @@ User is proposed to accept T&C and he accepts them
User is proposed to prove he is not a robot and he does it:
> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -396,9 +412,11 @@ Some homeservers may require the user to enter MSISDN.
On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration.
The user enter a phone number and select a country, the `client_secret` is generated by the application
The user enters a phone number and selects a country, the `client_secret` is generated by the application
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
```shell script
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
```
```json
{
@ -430,10 +448,11 @@ If it is not the case, the homeserver send the SMS and returns some data, especi
}
```
When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet:
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
When we execute the register request, with the received `sid`, we get an error since the MSISDN is not validated yet:
```shell script
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
"auth": {
@ -492,7 +511,9 @@ There is an issue on Synapse, which return a 401, it sends too much data along w
The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request:
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
```shell script
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
```
```json
{
@ -520,7 +541,9 @@ And if the code is correct we get a 200 with:
We can now execute the registration request, to the homeserver
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```shell script
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json
{
@ -535,7 +558,7 @@ We can now execute the registration request, to the homeserver
}
```
Now the homeserver consider that the `m.login.msisdn` step is completed (401):
Now the homeserver considers that the `m.login.msisdn` step is completed (401):
```json
{

View file

@ -18,9 +18,13 @@
package org.matrix.android.sdk.rx
import androidx.paging.PagedList
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
class RxSession(private val session: Session) {
@ -110,6 +110,11 @@ class RxSession(private val session: Session) {
.startWithCallable { session.getThreePids() }
}
fun livePendingThreePIds(): Observable<List<ThreePid>> {
return session.getPendingThreePidsLive().asObservable()
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}

View file

@ -115,7 +115,7 @@ dependencies {
def coroutines_version = "1.3.8"
def markwon_version = '3.1.0'
def daggerVersion = '2.25.4'
def work_version = '2.3.3'
def work_version = '2.4.0'
def retrofit_version = '2.6.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -144,7 +144,6 @@ dependencies {
// Image
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
implementation 'id.zelory:compressor:3.0.0'
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'

View file

@ -23,15 +23,12 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -96,9 +93,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
}
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
}
}

View file

@ -21,7 +21,6 @@ import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -29,6 +28,8 @@ import org.junit.runners.MethodSorters
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.ByteArrayOutputStream
import java.io.InputStream
/**
@ -52,17 +53,14 @@ class AttachmentEncryptionTest {
memoryFile.inputStream
}
val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo)
val decryptedStream = ByteArrayOutputStream()
val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream)
assertNotNull(decryptedStream)
assert(result)
val buffer = ByteArray(100)
val toByteArray = decryptedStream.toByteArray()
val len = decryptedStream!!.read(buffer)
decryptedStream.close()
return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
return Base64.encodeToString(toByteArray, 0, toByteArray.size, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
}
@Test

View file

@ -17,23 +17,22 @@
package org.matrix.android.sdk.internal.session.room.send
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import org.junit.Assert.assertEquals
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
/**
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
* we can add more tests to cover the edge cases.
* Some tests are suffixed with `_not_passing`, maybe one day we will fix them...
* Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the
* formatted body, which is quite useless.
* Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered.
* Element Web should be used as a reference for expected results, but not always.
* Also Element Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. We are doing
* the same to be able to edit messages (See #1939)
* See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
*/
@Suppress("SpellCheckingInspection")
@ -46,8 +45,7 @@ class MarkdownParserTest : InstrumentedTest {
*/
private val markdownParser = MarkdownParser(
Parser.builder().build(),
HtmlRenderer.builder().build(),
TextContentRenderer.builder().build()
HtmlRenderer.builder().build()
)
@Test
@ -83,6 +81,15 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseBoldNewLines() {
testTypeNewLines(
name = "bold",
markdownPattern = "**",
htmlExpectedTag = "strong"
)
}
@Test
fun parseItalic() {
testType(
@ -92,14 +99,23 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseItalicNewLines() {
testTypeNewLines(
name = "italic",
markdownPattern = "*",
htmlExpectedTag = "em"
)
}
@Test
fun parseItalic2() {
// Riot-Web format
"_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>")
// Element Web format
"_italic_".let { markdownParser.parse(it).expect(it, "<em>italic</em>") }
}
/**
* Note: the test is not passing, it does not work on Riot-Web neither
* Note: the test is not passing, it does not work on Element Web neither
*/
@Test
fun parseStrike_not_passing() {
@ -110,14 +126,30 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseStrikeNewLines() {
testTypeNewLines(
name = "strike",
markdownPattern = "~~",
htmlExpectedTag = "del"
)
}
@Test
fun parseCode() {
testType(
name = "code",
markdownPattern = "`",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCodeNewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "`",
htmlExpectedTag = "code"
)
}
@ -126,9 +158,16 @@ class MarkdownParserTest : InstrumentedTest {
testType(
name = "code",
markdownPattern = "``",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCode2NewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "``",
htmlExpectedTag = "code"
)
}
@ -137,78 +176,85 @@ class MarkdownParserTest : InstrumentedTest {
testType(
name = "code",
markdownPattern = "```",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCode3NewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "```",
htmlExpectedTag = "code"
)
}
@Test
fun parseUnorderedList() {
"- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
"- item1".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n</ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n<li>item2</li>\n</ul>") }
}
@Test
fun parseOrderedList() {
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
"1. item1".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n</ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n<li>item2</li>\n</ol>") }
}
@Test
fun parseHorizontalLine() {
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
"---".let { markdownParser.parse(it).expect(it, "<hr />") }
}
@Test
fun parseH2AndContent() {
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
"a\n---\nb".let { markdownParser.parse(it).expect(it, "<h2>a</h2>\n<p>b</p>") }
}
@Test
fun parseQuote() {
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
"> quoted".let { markdownParser.parse(it).expect(it, "<blockquote>\n<p>quoted</p>\n</blockquote>") }
}
@Test
fun parseQuote_not_passing() {
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
"> quoted\nline2".let { markdownParser.parse(it).expect(it, "<blockquote><p>quoted<br />line2</p></blockquote>") }
}
@Test
fun parseBoldItalic() {
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
"*italic* **bold**".let { markdownParser.parse(it).expect(it, "<em>italic</em> <strong>bold</strong>") }
"**bold** *italic*".let { markdownParser.parse(it).expect(it, "<strong>bold</strong> <em>italic</em>") }
}
@Test
fun parseHead() {
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
"# head1".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>") }
"## head2".let { markdownParser.parse(it).expect(it, "<h2>head2</h2>") }
"### head3".let { markdownParser.parse(it).expect(it, "<h3>head3</h3>") }
"#### head4".let { markdownParser.parse(it).expect(it, "<h4>head4</h4>") }
"##### head5".let { markdownParser.parse(it).expect(it, "<h5>head5</h5>") }
"###### head6".let { markdownParser.parse(it).expect(it, "<h6>head6</h6>") }
}
@Test
fun parseHeads() {
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
"# head1\n# head2".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>\n<h1>head2</h1>") }
}
@Test
fun parseBoldNewLines_not_passing() {
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") }
}
@Test
fun parseLinks() {
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
"[link](target)".let { markdownParser.parse(it).expect(it, """<a href="target">link</a>""") }
}
@Test
fun parseParagraph() {
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
"# head\ncontent".let { markdownParser.parse(it).expect(it, "<h1>head</h1>\n<p>content</p>") }
}
private fun testIdentity(text: String) {
@ -217,59 +263,93 @@ class MarkdownParserTest : InstrumentedTest {
private fun testType(name: String,
markdownPattern: String,
htmlExpectedTag: String,
plainTextPrefix: String = "",
plainTextSuffix: String = "") {
htmlExpectedTag: String) {
// Test simple case
"$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// Test twice the same tag
"$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
}
val textBefore = "a"
val textAfter = "b"
// With sticked text before
"$textBefore$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With text before and space
"$textBefore $markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With sticked text after
"$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With space and text after
"$markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
// With sticked text before and text after
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With text before and after, with spaces
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
}
private fun testTypeNewLines(name: String,
markdownPattern: String,
htmlExpectedTag: String) {
// With new line inside the block
"$markdownPattern$name\n$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name<br />$name</$htmlExpectedTag>")
}
// With new line between two blocks
"$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><$htmlExpectedTag>$name</$htmlExpectedTag>")
}
}
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {

View file

@ -26,13 +26,10 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -97,9 +94,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
}
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
}
}

View file

@ -45,7 +45,7 @@ sealed class WellknownResult {
/**
* Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter.
*/
object FailPrompt : WellknownResult()
data class FailPrompt(val homeServerUrl: String?, val wellKnown: WellKnown?) : WellknownResult()
/**
* Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process.

View file

@ -33,7 +33,7 @@ interface ContentUploadStateTracker {
object Idle : State()
object EncryptingThumbnail : State()
data class UploadingThumbnail(val current: Long, val total: Long) : State()
object Encrypting : State()
data class Encrypting(val current: Long, val total: Long) : State()
data class Uploading(val current: Long, val total: Long) : State()
object Success : State()
data class Failure(val throwable: Throwable) : State()

View file

@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean {
}
}
fun Event.isAudioMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_AUDIO -> true
else -> false
}
}
fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean {
else -> false
}
}
fun Event.isAttachmentMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) {

View file

@ -83,4 +83,43 @@ interface ProfileService {
* @param refreshData set to true to fetch data from the homeserver
*/
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
/**
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
*/
fun getPendingThreePids(): List<ThreePid>
/**
* Get the pending 3Pids Live
*/
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
/**
* Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list.
*/
fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Validate a code received by text message
*/
fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
*/
fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Cancel adding a threepid. It will remove locally stored data about this ThreePid
*/
fun cancelAddingThreePid(threePid: ThreePid,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Remove a 3Pid from the Matrix account.
*/
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
}

View file

@ -110,13 +110,13 @@ interface SendService {
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
fun resendTextMessage(localEcho: TimelineEvent): Cancelable
/**
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
/**
* Remove this failed message from the timeline
@ -124,8 +124,16 @@ interface SendService {
*/
fun deleteFailedEcho(localEcho: TimelineEvent)
/**
* Delete all the events in one of the sending states
*/
fun clearSendingQueue()
/**
* Cancel sending a specific event. It has to be in one of the sending states
*/
fun cancelSend(eventId: String)
/**
* Resend all failed messages one by one (and keep order)
*/

View file

@ -37,7 +37,8 @@ enum class SendState {
internal companion object {
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
val IS_SENT_STATES = listOf(SENT, SYNCED)
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING)
val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
}
@ -45,5 +46,7 @@ enum class SendState {
fun hasFailed() = HAS_FAILED_STATES.contains(this)
fun isInProgress() = IS_PROGRESSING_STATES.contains(this)
fun isSending() = IS_SENDING_STATES.contains(this)
}

View file

@ -17,6 +17,9 @@
package org.matrix.android.sdk.internal.auth.registration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
/**
* This class execute the registration request and is responsible to keep the session of interactive authentication
@ -193,7 +193,7 @@ internal class DefaultRegistrationWizard(
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid")
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
val validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,

View file

@ -20,9 +20,14 @@ package org.matrix.android.sdk.internal.crypto.attachments
import android.util.Base64
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.util.base64ToBase64Url
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
import org.matrix.android.sdk.internal.util.base64UrlToBase64
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.Cipher
@ -35,8 +40,121 @@ internal object MXEncryptedAttachments {
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()
val initVectorBytes = ByteArray(16) { 0.toByte() }
val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart)
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
val key = ByteArray(32)
secureRandom.nextBytes(key)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
outputFile.outputStream().use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
clearStream.use { inputStream ->
val estimatedSize = inputStream.available()
progress.invoke(0, estimatedSize)
read = inputStream.read(data)
var totalRead = read
while (read != -1) {
progress.invoke(totalRead, estimatedSize)
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
read = inputStream.read(data)
totalRead += read
}
}
// encrypt the latest chunk
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
}
return EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair<DigestInputStream, EncryptedFileInfo> {
// val secureRandom = SecureRandom()
//
// // generate a random iv key
// // Half of the IV is random, the lower order bits are zeroed
// // such that the counter never wraps.
// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
// val initVectorBytes = ByteArray(16) { 0.toByte() }
//
// val ivRandomPart = ByteArray(8)
// secureRandom.nextBytes(ivRandomPart)
//
// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
//
// val key = ByteArray(32)
// secureRandom.nextBytes(key)
//
// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
// val ivParameterSpec = IvParameterSpec(initVectorBytes)
// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
//
// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher)
//
// // Could it be possible to get the digest on the fly instead of
// val info = EncryptedFileInfo(
// url = null,
// mimetype = mimetype,
// key = EncryptedFileKey(
// alg = "A256CTR",
// ext = true,
// key_ops = listOf("encrypt", "decrypt"),
// kty = "oct",
// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
// ),
// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
// v = "v2"
// )
//
// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
// return DigestInputStream(cipherInputStream, messageDigest) to info
// }
//
// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo {
// return info.copy(
// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT)))
// )
// }
/***
* Encrypt an attachment stream.
* DO NOT USE for big files, it will load all in memory
* @param attachmentStream the attachment stream. Will be closed after this method call.
* @param mimetype the mime type
* @return the encryption file info
@ -59,14 +177,14 @@ internal object MXEncryptedAttachments {
val key = ByteArray(32)
secureRandom.nextBytes(key)
ByteArrayOutputStream().use { outputStream ->
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val byteArrayOutputStream = ByteArrayOutputStream()
byteArrayOutputStream.use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
@ -85,44 +203,26 @@ internal object MXEncryptedAttachments {
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = outputStream.toByteArray()
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
}
/**
* Decrypt an attachment
*
* @param attachmentStream the attachment stream. Will be closed after this method call.
* @param encryptedFileInfo the encryption file info
* @return the decrypted attachment stream
*/
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? {
if (encryptedFileInfo?.isValid() != true) {
Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields")
return null
}
val elementToDecrypt = encryptedFileInfo.toElementToDecrypt()
return decryptAttachment(attachmentStream, elementToDecrypt)
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = byteArrayOutputStream.toByteArray()
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
/**
@ -130,84 +230,61 @@ internal object MXEncryptedAttachments {
*
* @param attachmentStream the attachment stream. Will be closed after this method call.
* @param elementToDecrypt the elementToDecrypt info
* @return the decrypted attachment stream
* @param outputStream the outputStream where the decrypted attachment will be write.
* @return true in case of success, false in case of error
*/
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? {
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean {
// sanity checks
if (null == attachmentStream || elementToDecrypt == null) {
Timber.e("## decryptAttachment() : null stream")
return null
return false
}
val t0 = System.currentTimeMillis()
ByteArrayOutputStream().use { outputStream ->
try {
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
try {
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
attachmentStream.use { inputStream ->
attachmentStream.use { inputStream ->
read = inputStream.read(data)
while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outputStream.write(decodedBytes)
read = inputStream.read(data)
while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outputStream.write(decodedBytes)
read = inputStream.read(data)
}
}
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outputStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return null
}
return outputStream.toByteArray().inputStream()
.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() failed: OOM")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() failed")
}
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outputStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return false
}
return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() failed: OOM")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() failed")
}
return null
}
/**
* Base64 URL conversion methods
*/
private fun base64UrlToBase64(base64Url: String): String {
return base64Url.replace('-', '+')
.replace('_', '/')
}
internal fun base64ToBase64Url(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')
.replace("=", "")
}
private fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("=", "")
return false
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 org.matrix.android.sdk.internal.crypto.attachments
import android.util.Base64
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
class MatrixDigestCheckInputStream(
inputStream: InputStream?,
private val expectedDigest: String
) : FilterInputStream(inputStream) {
private val digest = MessageDigest.getInstance("SHA-256")
@Throws(IOException::class)
override fun read(): Int {
val b = `in`.read()
if (b >= 0) {
digest.update(b.toByte())
}
if (b == -1) {
ensureDigest()
}
return b
}
@Throws(IOException::class)
override fun read(
b: ByteArray,
off: Int,
len: Int): Int {
val n = `in`.read(b, off, len)
if (n > 0) {
digest.update(b, off, n)
}
if (n == -1) {
ensureDigest()
}
return n
}
@Throws(IOException::class)
private fun ensureDigest() {
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT))
if (currentDigestValue != expectedDigest) {
throw IOException("Bad digest")
}
}
}

View file

@ -55,14 +55,14 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot)
32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat)
33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses)
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench)
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_spanner, R.drawable.ic_verification_spanner)
35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa)
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up)
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbs_up, R.drawable.ic_verification_thumbs_up)
37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella)
38 -> EmojiRepresentation("", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass)
39 -> EmojiRepresentation("", R.string.verification_emoji_clock, R.drawable.ic_verification_clock)
40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift)
41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb)
41 -> EmojiRepresentation("💡", R.string.verification_emoji_light_bulb, R.drawable.ic_verification_light_bulb)
42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book)
43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil)
44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip)
@ -74,7 +74,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag)
51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train)
52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle)
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane)
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_aeroplane, R.drawable.ic_verification_aeroplane)
54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket)
55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy)
56 -> EmojiRepresentation("", R.string.verification_emoji_ball, R.drawable.ic_verification_ball)
@ -82,7 +82,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet)
59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell)
60 -> EmojiRepresentation("", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor)
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone)
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphones, R.drawable.ic_verification_headphones)
62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder)
/* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin)
}

View file

@ -20,18 +20,24 @@ package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 4L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -63,4 +69,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4")
realm.schema.create("PendingThreePidEntity")
.addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
.addField(PendingThreePidEntityFields.EMAIL, String::class.java)
.addField(PendingThreePidEntityFields.MSISDN, String::class.java)
.addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
.addField(PendingThreePidEntityFields.SID, String::class.java)
.setRequired(PendingThreePidEntityFields.SID, true)
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
}
}

View file

@ -19,13 +19,14 @@ package org.matrix.android.sdk.internal.database
import android.content.Context
import androidx.core.content.edit
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserMd5
import org.matrix.android.sdk.internal.session.SessionModule
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@ -46,20 +47,16 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
val migration: RealmSessionStoreMigration,
context: Context) {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 3L
}
// Keep legacy preferences name for compatibility reason
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration {
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) {
Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.")
Timber.v("The file has been deleted to recover.")
Timber.v("************************************************************")
Timber.e("************************************************************")
Timber.e("The realm file session was corrupted and couldn't be loaded.")
Timber.e("The file has been deleted to recover.")
Timber.e("************************************************************")
deleteRealmFiles()
}
sharedPreferences.edit {
@ -74,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.schemaVersion(SESSION_STORE_SCHEMA_VERSION)
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
.migration(migration)
.build()
@ -90,6 +87,11 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
// Delete all the realm files of the session
private fun deleteRealmFiles() {
if (BuildConfig.DEBUG) {
Timber.e("No op because it is a debug build")
return
}
listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file ->
try {
File(directory, file).deleteRecursively()

View file

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
/**
* This class is used to store pending threePid data, when user wants to add a threePid to his account
*/
internal open class PendingThreePidEntity(
var email: String? = null,
var msisdn: String? = null,
var clientSecret: String = "",
var sendAttempt: Int = 0,
var sid: String = "",
var submitUrl: String? = null
) : RealmObject()

View file

@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
PendingThreePidEntity::class,
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,

View file

@ -24,6 +24,7 @@ import okio.BufferedSink
import okio.ForwardingSink
import okio.Sink
import okio.buffer
import org.matrix.android.sdk.api.extensions.tryThis
import java.io.IOException
internal class ProgressRequestBody(private val delegate: RequestBody,
@ -35,15 +36,13 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
return delegate.contentType()
}
override fun contentLength(): Long {
try {
return delegate.contentLength()
} catch (e: IOException) {
e.printStackTrace()
}
override fun isOneShot() = delegate.isOneShot()
return -1
}
override fun isDuplex() = delegate.isDuplex()
val length = tryThis { delegate.contentLength() } ?: -1
override fun contentLength() = length
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {

View file

@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt)
Timber.v("## FileService: decrypt file")
val decryptSuccess = MXEncryptedAttachments.decryptAttachment(
source.inputStream(),
elementToDecrypt,
destFile.outputStream().buffered()
)
response.close()
if (decryptedStream == null) {
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
} else {
decryptedStream.use {
writeToFile(decryptedStream, destFile)
}
}
} else {
writeToFile(source.inputStream(), destFile)
response.close()
}
} else {
Timber.v("## FileService: cache hit for $url")
}
Try.just(copyFile(destFile, downloadMode))

View file

@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
updateState(key, progressData)
}
internal fun setEncrypting(key: String) {
val progressData = ContentUploadStateTracker.State.Encrypting
internal fun setEncrypting(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.Encrypting(current, total)
updateState(key, progressData)
}

View file

@ -23,13 +23,16 @@ import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import okio.source
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.network.ProgressRequestBody
@ -38,6 +41,7 @@ import org.matrix.android.sdk.internal.network.toFailure
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.UUID
import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated
@ -54,7 +58,21 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
val uploadBody = object : RequestBody() {
override fun contentLength() = file.length()
// Disable okhttp auto resend for 'large files'
override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000
override fun contentType(): MediaType? {
return mimeType?.toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
file.source().use { sink.writeAll(it) }
}
}
return upload(uploadBody, filename, progressListener)
}
@ -70,14 +88,18 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
return withContext(Dispatchers.IO) {
val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
inputStream.use {
uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
}
val inputStream = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)
} ?: throw FileNotFoundException()
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
workingFile.outputStream().use {
inputStream.copyTo(it)
}
return uploadFile(workingFile, filename, mimeType, progressListener).also {
tryThis { workingFile.delete() }
}
}
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.content
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import androidx.exifinterface.media.ExifInterface
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.util.UUID
import javax.inject.Inject
internal class ImageCompressor @Inject constructor() {
suspend fun compress(
context: Context,
imageFile: File,
desiredWidth: Int,
desiredHeight: Int,
desiredQuality: Int = 80): File {
return withContext(Dispatchers.IO) {
val compressedBitmap = BitmapFactory.Options().run {
inJustDecodeBounds = true
decodeBitmap(imageFile, this)
inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight)
inJustDecodeBounds = false
decodeBitmap(imageFile, this)?.let {
rotateBitmap(imageFile, it)
}
} ?: return@withContext imageFile
val destinationFile = createDestinationFile(context)
runCatching {
destinationFile.outputStream().use {
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
}
}
return@withContext destinationFile
}
}
private fun rotateBitmap(file: File, bitmap: Bitmap): Bitmap {
file.inputStream().use { inputStream ->
try {
ExifInterface(inputStream).let { exifInfo ->
val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.preRotate(-90f)
matrix.preScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.preRotate(90f)
matrix.preScale(-1f, 1f)
}
else -> return bitmap
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
} catch (e: Exception) {
Timber.e(e, "Cannot read orientation")
}
}
return bitmap
}
// https://developer.android.com/topic/performance/graphics/load-bitmap
private fun calculateInSampleSize(width: Int, height: Int, desiredWidth: Int, desiredHeight: Int): Int {
var inSampleSize = 1
if (width > desiredWidth || height > desiredHeight) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
private fun decodeBitmap(file: File, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? {
return try {
file.inputStream().use { inputStream ->
BitmapFactory.decodeStream(inputStream, null, options)
}
} catch (e: Exception) {
Timber.e(e, "Cannot decode Bitmap")
null
}
}
private fun createDestinationFile(context: Context): File {
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
}
}

View file

@ -22,8 +22,7 @@ import android.graphics.BitmapFactory
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import id.zelory.compressor.Compressor
import id.zelory.compressor.constraint.default
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent
@ -37,6 +36,7 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.network.ProgressRequestBody
import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -71,6 +71,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
@Inject lateinit var fileUploader: FileUploader
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
@Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject lateinit var imageCompressor: ImageCompressor
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
@ -98,9 +100,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
val attachment = params.attachment
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
if (allCancelled) {
// there is no point in uploading the image!
return Result.success(inputData)
.also { Timber.e("## Send: Work cancelled by user") }
}
var newImageAttributes: NewImageAttributes? = null
val attachment = params.attachment
val filesToDelete = mutableListOf<File>()
try {
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
@ -112,124 +120,100 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
)
)
inputStream.use {
var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
}
}
try {
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
} else {
fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
}
uploadedThumbnailUrl = contentUploadResponse.contentUri
} catch (t: Throwable) {
Timber.e(t, "Thumbnail update failed")
}
}
val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) {
if (isStopped) {
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
} else {
contentUploadStateTracker.setProgress(it, current, total)
}
}
}
}
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try {
// Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should
// copy it to a cache folder by using InputStream and OutputStream.
// https://github.com/zetbaitsu/Compressor/pull/150
// As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile.
var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir)
cacheFile.parentFile?.mkdirs()
if (cacheFile.exists()) {
cacheFile.delete()
}
cacheFile.createNewFile()
cacheFile.deleteOnExit()
val outputStream = cacheFile.outputStream()
outputStream.use {
inputStream.copyTo(outputStream)
}
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
cacheFile = Compressor.compress(context, cacheFile) {
default(
width = MAX_IMAGE_SIZE,
height = MAX_IMAGE_SIZE
)
}.also { compressedFile ->
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
val fileSize = compressedFile.length().toInt()
newImageAttributes = NewImageAttributes(
options.outWidth,
options.outHeight,
fileSize
)
}
}
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(cacheFile.inputStream(), attachment.getSafeMimeType())
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else {
fileUploader
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener)
}
// If it's a file update the file service so that it does not redownload?
if (params.attachment.type == ContentAttachmentData.Type.FILE) {
context.contentResolver.openInputStream(attachment.queryUri)?.let {
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
}
}
handleSuccess(params,
contentUploadResponse.contentUri,
uploadedFileEncryptedFileInfo,
uploadedThumbnailUrl,
uploadedThumbnailEncryptedFileInfo,
newImageAttributes)
} catch (t: Throwable) {
Timber.e(t)
handleFailure(params, t)
// always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
.also { filesToDelete.add(it) }
workingFile.outputStream().use { outputStream ->
inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
val uploadThumbnailResult = dealWithThumbnail(params)
val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) {
if (isStopped) {
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
} else {
contentUploadStateTracker.setProgress(it, current, total)
}
}
}
}
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try {
val fileToUpload: File
var newImageAttributes: NewImageAttributes? = null
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.also { compressedFile ->
// Get new Bitmap size
compressedFile.inputStream().use {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
val bitmap = BitmapFactory.decodeStream(it, null, options)
val fileSize = bitmap?.byteCount ?: 0
newImageAttributes = NewImageAttributes(
options.outWidth,
options.outHeight,
fileSize
)
}
}
.also { filesToDelete.add(it) }
} else {
fileToUpload = workingFile
}
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## FileService: Encrypt file")
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
.also { filesToDelete.add(it) }
uploadedFileEncryptedFileInfo =
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
notifyTracker(params) {
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
}
}
Timber.v("## FileService: Uploading file")
fileUploader
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
} else {
Timber.v("## FileService: Clear file")
fileUploader
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
}
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
try {
context.contentResolver.openInputStream(attachment.queryUri)?.let {
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
}
Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update file cache")
}
handleSuccess(params,
contentUploadResponse.contentUri,
uploadedFileEncryptedFileInfo,
uploadThumbnailResult?.uploadedThumbnailUrl,
uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo,
newImageAttributes)
} catch (t: Throwable) {
Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
handleFailure(params, t)
}
} catch (e: Exception) {
Timber.e(e)
Timber.e(e, "## FileService: ERROR")
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success(
WorkerParamsFactory.toData(
@ -238,9 +222,61 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
)
)
)
} finally {
// Delete all temporary files
filesToDelete.forEach {
tryThis { it.delete() }
}
}
}
private data class UploadThumbnailResult(
val uploadedThumbnailUrl: String,
val uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo?
)
/**
* If appropriate, it will create and upload a thumbnail
*/
private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? {
return ThumbnailExtractor.extractThumbnail(context, params.attachment)
?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
}
}
try {
if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${params.attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
UploadThumbnailResult(
contentUploadResponse.contentUri,
encryptionResult.encryptedFileInfo
)
} else {
val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${params.attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
UploadThumbnailResult(
contentUploadResponse.contentUri,
null
)
}
} catch (t: Throwable) {
Timber.e(t, "Thumbnail upload failed")
null
}
}
}
private fun handleFailure(params: Params, failure: Throwable): Result {
notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) }
@ -259,7 +295,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Result {
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
val updatedEvents = params.events
@ -268,7 +303,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
}
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams))
return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
}
}
private fun updateEvent(event: Event,

View file

@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
// private fun URL.toKey() = toString()
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
if (done) {
updateState(url, ContentDownloadStateTracker.State.Success)
} else {
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
mainHandler.post {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
if (done) {
updateState(url, ContentDownloadStateTracker.State.Success)
} else {
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
}
}
}
override fun error(url: String, errorCode: Int) {
Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
mainHandler.post {
Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
}
}
}

View file

@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.identity.FoundThreePid
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url
import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
@ -32,6 +31,7 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetail
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.base64ToBase64Url
import java.util.Locale
import javax.inject.Inject

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddEmailBody(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
* 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The email address to validate.
*/
@Json(name = "email")
val email: String,
/**
* Required. The server will only send an email if the send_attempt is a number greater than the most
* recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly
* sending the same email in the case of request retries between the POSTing user and the identity server.
* The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
* If they do not, the server should respond with success but not resend the email.
*/
@Json(name = "send_attempt")
val sendAttempt: Int
)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddEmailResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely
* of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String
)

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddMsisdnBody(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
* 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in
* phone_number should be parsed as if it were dialled from.
*/
@Json(name = "country")
val country: String,
/**
* Required. The phone number to validate.
*/
@Json(name = "phone_number")
val phoneNumber: String,
/**
* Required. The server will only send an SMS if the send_attempt is a number greater than the most
* recent one which it has seen, scoped to that country + phone_number + client_secret triple. This
* is to avoid repeatedly sending the same SMS in the case of request retries between the POSTing user
* and the identity server. The client should increment this value if they desire a new SMS (e.g. a
* reminder) to be sent.
*/
@Json(name = "send_attempt")
val sendAttempt: Int
)

View file

@ -0,0 +1,55 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddMsisdnResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
* Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String,
/**
* An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
* Service API's POST /validate/email/submitToken endpoint (without the requirement for an access token).
* The homeserver must send this token to the user (if applicable), who should then be prompted to provide it to the client.
*
* If this field is not present, the client can assume that verification will happen without the client's involvement provided
* the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
*/
@Json(name = "submit_url")
val submitUrl: String? = null,
/* ==========================================================================================
* It seems that the homeserver is sending more data, we may need it
* ========================================================================================== */
@Json(name = "msisdn")
val msisdn: String? = null,
@Json(name = "intl_fmt")
val formattedMsisdn: String? = null,
@Json(name = "success")
val success: Boolean? = null
)

View file

@ -0,0 +1,111 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.UUID
import javax.inject.Inject
internal abstract class AddThreePidTask : Task<AddThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultAddThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
private val eventBus: EventBus) : AddThreePidTask() {
override suspend fun execute(params: Params) {
when (params.threePid) {
is ThreePid.Email -> addEmail(params.threePid)
is ThreePid.Msisdn -> addMsisdn(params.threePid)
}
}
private suspend fun addEmail(threePid: ThreePid.Email) {
val clientSecret = UUID.randomUUID().toString()
val sendAttempt = 1
val result = executeRequest<AddEmailResponse>(eventBus) {
val body = AddEmailBody(
clientSecret = clientSecret,
email = threePid.email,
sendAttempt = sendAttempt
)
apiCall = profileAPI.addEmail(body)
}
// Store as a pending three pid
monarchy.awaitTransaction { realm ->
PendingThreePid(
threePid = threePid,
clientSecret = clientSecret,
sendAttempt = sendAttempt,
sid = result.sid,
submitUrl = null
)
.let { pendingThreePidMapper.map(it) }
.let { realm.copyToRealm(it) }
}
}
private suspend fun addMsisdn(threePid: ThreePid.Msisdn) {
val clientSecret = UUID.randomUUID().toString()
val sendAttempt = 1
// Get country code and national number from the phone number
val phoneNumber = threePid.msisdn
val phoneNumberUtil = PhoneNumberUtil.getInstance()
val parsedNumber = phoneNumberUtil.parse(phoneNumber, null)
val countryCode = parsedNumber.countryCode
val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode)
val result = executeRequest<AddMsisdnResponse>(eventBus) {
val body = AddMsisdnBody(
clientSecret = clientSecret,
country = country,
phoneNumber = parsedNumber.nationalNumber.toString(),
sendAttempt = sendAttempt
)
apiCall = profileAPI.addMsisdn(body)
}
// Store as a pending three pid
monarchy.awaitTransaction { realm ->
PendingThreePid(
threePid = threePid,
clientSecret = clientSecret,
sendAttempt = sendAttempt,
sid = result.sid,
submitUrl = result.submitUrl
)
.let { pendingThreePidMapper.map(it) }
.let { realm.copyToRealm(it) }
}
}
}

View file

@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
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.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
@ -44,6 +45,11 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
private val getProfileInfoTask: GetProfileInfoTask,
private val setDisplayNameTask: SetDisplayNameTask,
private val setAvatarUrlTask: SetAvatarUrlTask,
private val addThreePidTask: AddThreePidTask,
private val validateSmsCodeTask: ValidateSmsCodeTask,
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
private val deleteThreePidTask: DeleteThreePidTask,
private val pendingThreePidMapper: PendingThreePidMapper,
private val fileUploader: FileUploader) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
if (refreshData) {
// Force a refresh of the values
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
refreshThreePids()
}
return monarchy.findAllMappedWithChanges(
@ -126,6 +130,95 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
{ it.asDomain() }
)
}
private fun refreshThreePids() {
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
}
override fun getPendingThreePids(): List<ThreePid> {
return monarchy.fetchAllMappedSync(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
return monarchy.findAllMappedWithChanges(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return addThreePidTask
.configureWith(AddThreePidTask.Params(threePid)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return validateSmsCodeTask
.configureWith(ValidateSmsCodeTask.Params(threePid, code)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = uiaSession,
accountPassword = accountPassword,
userWantsToCancel = false
)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = null,
accountPassword = null,
userWantsToCancel = true
)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
/**
* Wrap the callback to fetch 3Pids from the server in case of success
*/
private fun alsoRefresh(callback: MatrixCallback<Unit>): MatrixCallback<Unit> {
return object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
refreshThreePids()
callback.onSuccess(data)
}
}
}
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return deleteThreePidTask
.configureWith(DeleteThreePidTask.Params(threePid)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
}
private fun UserThreePidEntity.asDomain(): ThreePid {

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidBody(
/**
* Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"]
*/
@Json(name = "medium") val medium: String,
/**
* Required. The third party address being removed.
*/
@Json(name = "address") val address: String
)

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidResponse(
/**
* Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from
* the identity server. success indicates that the identity server has unbound the identifier
* whereas no-support indicates that the identity server refuses to support the request or the
* homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"]
*/
@Json(name = "id_server_unbind_result")
val idServerUnbindResult: String? = null
)

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class DeleteThreePidTask : Task<DeleteThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultDeleteThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
private val eventBus: EventBus) : DeleteThreePidTask() {
override suspend fun execute(params: Params) {
executeRequest<DeleteThreePidResponse>(eventBus) {
val body = DeleteThreePidBody(
medium = params.threePid.toMedium(),
address = params.threePid.value
)
apiCall = profileAPI.deleteThreePid(body)
}
// We do not really care about the result for the moment
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody(
/**
* Required. The client secret used in the session with the homeserver.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The session identifier given by the homeserver.
*/
@Json(name = "sid")
val sid: String,
/**
* Additional authentication information for the user-interactive authentication API.
*/
@Json(name = "auth")
val auth: UserPasswordAuth?
)

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid,
val session: String?,
val accountPassword: String?,
val userWantsToCancel: Boolean
)
}
internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
@UserId private val userId: String,
private val eventBus: EventBus) : FinalizeAddingThreePidTask() {
override suspend fun execute(params: Params) {
if (params.userWantsToCancel.not()) {
// Get the required pending data
val pendingThreePids = monarchy.fetchAllMappedSync(
{ it.where(PendingThreePidEntity::class.java) },
{ pendingThreePidMapper.map(it) }
)
.firstOrNull { it.threePid == params.threePid }
?: throw IllegalArgumentException("unknown threepid")
try {
executeRequest<Unit>(eventBus) {
val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) {
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
)
apiCall = profileAPI.finalizeAddThreePid(body)
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
}
cleanupDatabase(params)
}
private suspend fun cleanupDatabase(params: Params) {
// Delete the pending three pid
monarchy.awaitTransaction { realm ->
realm.where(PendingThreePidEntity::class.java)
.equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value)
.or()
.equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value)
.findAll()
.deleteAllFromRealm()
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
internal data class PendingThreePid(
val threePid: ThreePid,
val clientSecret: String,
val sendAttempt: Int,
// For Msisdn and Email
val sid: String,
// For Msisdn only
val submitUrl: String?
)

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import javax.inject.Inject
internal class PendingThreePidMapper @Inject constructor() {
fun map(entity: PendingThreePidEntity): PendingThreePid {
return PendingThreePid(
threePid = entity.email?.let { ThreePid.Email(it) }
?: entity.msisdn?.let { ThreePid.Msisdn(it) }
?: error("Invalid data"),
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
sid = entity.sid,
submitUrl = entity.submitUrl
)
}
fun map(domain: PendingThreePid): PendingThreePidEntity {
return PendingThreePidEntity(
email = domain.threePid.takeIf { it is ThreePid.Email }?.value,
msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value,
clientSecret = domain.clientSecret,
sendAttempt = domain.sendAttempt,
sid = domain.sid,
submitUrl = domain.submitUrl
)
}
}

View file

@ -19,6 +19,8 @@
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@ -26,9 +28,9 @@ import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Url
internal interface ProfileAPI {
/**
* Get the combined profile information for this user.
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
@ -71,4 +73,35 @@ internal interface ProfileAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken")
fun addEmail(@Body body: AddEmailBody): Call<AddEmailResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken")
fun addMsisdn(@Body body: AddMsisdnBody): Call<AddMsisdnResponse>
/**
* Validate Msisdn code (same model than for Identity server API)
*/
@POST
fun validateMsisdn(@Url url: String,
@Body params: ValidationCodeBody): Call<SuccessResult>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add")
fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call<Unit>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete")
fun deleteThreePid(@Body body: DeleteThreePidBody): Call<DeleteThreePidResponse>
}

View file

@ -58,4 +58,16 @@ internal abstract class ProfileModule {
@Binds
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
@Binds
abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask
@Binds
abstract fun bindValidateSmsCodeTask(task: DefaultValidateSmsCodeTask): ValidateSmsCodeTask
@Binds
abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask
@Binds
abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ValidateSmsCodeTask : Task<ValidateSmsCodeTask.Params, Unit> {
data class Params(
val threePid: ThreePid.Msisdn,
val code: String
)
}
internal class DefaultValidateSmsCodeTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase
private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
private val eventBus: EventBus
) : ValidateSmsCodeTask {
override suspend fun execute(params: ValidateSmsCodeTask.Params) {
// Search the pending ThreePid
val pendingThreePids = monarchy.fetchAllMappedSync(
{ it.where(PendingThreePidEntity::class.java) },
{ pendingThreePidMapper.map(it) }
)
.firstOrNull { it.threePid == params.threePid }
?: throw IllegalArgumentException("unknown threepid")
val url = pendingThreePids.submitUrl ?: throw IllegalArgumentException("invalid threepid")
val body = ValidationCodeBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
code = params.code
)
val result = executeRequest<SuccessResult>(eventBus) {
apiCall = profileAPI.validateMsisdn(url, body)
}
if (!result.isSuccess()) {
throw Failure.SuccessError
}
}
}

View file

@ -20,6 +20,8 @@ package org.matrix.android.sdk.internal.session.room
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
import org.matrix.android.sdk.api.session.room.RoomService
@ -75,9 +77,6 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import retrofit2.Retrofit
@Module
@ -105,14 +104,6 @@ internal abstract class RoomModule {
.builder()
.build()
}
@Provides
@JvmStatic
fun providesTextContentRenderer(): TextContentRenderer {
return TextContentRenderer
.builder()
.build()
}
}
@Binds

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.send
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
/**
* We cannot use work manager cancellation mechanism because cancelling a work will just ignore
* any follow up send that was already queued.
* We use this class to track cancel requests, the workers will look for this to check for cancellation request
* and just ignore the work request and continue by returning success.
*
* Known limitation, for now requests are not persisted
*/
@SessionScope
internal class CancelSendTracker @Inject constructor() {
data class Request(
val localId: String,
val roomId: String
)
private val cancellingRequests = ArrayList<Request>()
fun markLocalEchoForCancel(eventId: String, roomId: String) {
synchronized(cancellingRequests) {
cancellingRequests.add(Request(eventId, roomId))
}
}
fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean {
val index = synchronized(cancellingRequests) {
cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
}
return index != -1
}
fun markCancelled(eventId: String, roomId: String) {
synchronized(cancellingRequests) {
val index = cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
if (index != -1) {
cancellingRequests.removeAt(index)
}
}
}
}

View file

@ -17,24 +17,35 @@
package org.matrix.android.sdk.internal.session.room.send
import android.net.Uri
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@ -44,7 +55,6 @@ import org.matrix.android.sdk.internal.util.CancelableWork
import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@ -60,7 +70,8 @@ internal class DefaultSendService @AssistedInject constructor(
private val cryptoService: CryptoService,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository,
private val roomEventSender: RoomEventSender
private val roomEventSender: RoomEventSender,
private val cancelSendTracker: CancelSendTracker
) : SendService {
@AssistedInject.Factory
@ -127,48 +138,83 @@ internal class DefaultSendService @AssistedInject constructor(
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
return null
return NoOpCancellable
}
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) {
// TODO this need a refactoring of attachement sending
// val clearContent = localEcho.root.getClearContent()
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null
// when (messageContent.type) {
// MessageType.MSGTYPE_IMAGE -> {
// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null
// val url = imageContent.url ?: return null
// if (url.startsWith("mxc://")) {
// //TODO
// } else {
// //The image has not yet been sent
// val attachmentData = ContentAttachmentData(
// size = imageContent.info!!.size.toLong(),
// mimeType = imageContent.info.mimeType!!,
// width = imageContent.info.width.toLong(),
// height = imageContent.info.height.toLong(),
// name = imageContent.body,
// path = imageContent.url,
// type = ContentAttachmentData.Type.IMAGE
// )
// monarchy.runTransactionSync {
// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let {
// it.sendState = SendState.UNSENT
// }
// }
// return internalSendMedia(localEcho.root,attachmentData)
// }
// }
// }
return null
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.sendState.hasFailed()) {
val clearContent = localEcho.root.getClearContent()
val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent ?: return NoOpCancellable
val url = messageContent.getFileUrl() ?: return NoOpCancellable
if (url.startsWith("mxc://")) {
// We need to resend only the message as the attachment is ok
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
// we need to resend the media
return when (messageContent) {
is MessageImageContent -> {
// The image has not yet been sent
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size.toLong(),
mimeType = messageContent.info.mimeType!!,
width = messageContent.info.width.toLong(),
height = messageContent.info.height.toLong(),
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.IMAGE
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageVideoContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.videoInfo?.size ?: 0L,
mimeType = messageContent.mimeType,
width = messageContent.videoInfo?.width?.toLong(),
height = messageContent.videoInfo?.height?.toLong(),
duration = messageContent.videoInfo?.duration?.toLong(),
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.VIDEO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageFileContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size,
mimeType = messageContent.info.mimeType!!,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageAudioContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.audioInfo?.size ?: 0,
duration = messageContent.audioInfo?.duration?.toLong() ?: 0L,
mimeType = messageContent.audioInfo?.mimeType,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
else -> NoOpCancellable
}
}
return null
return NoOpCancellable
}
override fun deleteFailedEcho(localEcho: TimelineEvent) {
@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, eventId)
}
}
override fun resendAllFailedMessages() {
taskExecutor.executorScope.launch {
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
eventsToResend.forEach {
sendEvent(it)
if (it.root.isTextMessage()) {
resendTextMessage(it)
} else if (it.root.isAttachmentMessage()) {
resendMediaMessage(it)
}
}
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT)
}
}
// override fun failAllPendingMessages() {
// taskExecutor.executorScope.launch {
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
// }
// }
override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable {

View file

@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
@Inject lateinit var crypto: CryptoService
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result {
Timber.v("Start Encrypt work")
@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("Start Encrypt work for event ${params.event.eventId}")
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}")
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
if (localEvent.eventId == null) {
return Result.success()
}
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
return Result.success()
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
}
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()

View file

@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.database.helper.nextId
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -88,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}")
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
@ -114,9 +113,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
deleteFailedEcho(roomId, localEcho.eventId)
}
suspend fun deleteFailedEcho(roomId: String, eventId: String?) {
monarchy.awaitTransaction { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
roomSummaryUpdater.updateSendingInformation(realm, roomId)
}
}
@ -142,45 +145,47 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
fun getAllFailedEventsToResend(roomId: String): List<Event> {
fun getAllFailedEventsToResend(roomId: String): List<TimelineEvent> {
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
}
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES)
.findAllInRoomWithSendStates(realm, roomId, states)
.sortedByDescending { it.displayIndex }
.mapNotNull { it.root?.asDomain() }
.mapNotNull { it?.let { timelineEventMapper.map(it) } }
.filter { event ->
when (event.getClearType()) {
when (event.root.getClearType()) {
EventType.MESSAGE,
EventType.REDACTION,
EventType.REACTION -> {
val content = event.getClearContent().toModel<MessageContent>()
val content = event.root.getClearContent().toModel<MessageContent>()
if (content != null) {
when (content.msgType) {
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_LOCATION,
MessageType.MSGTYPE_TEXT -> {
true
}
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO -> {
// need to resend the attachment
false
true
}
else -> {
Timber.e("Cannot resend message ${event.type} / ${content.msgType}")
Timber.e("Cannot resend message ${event.root.getClearType()} / ${content.msgType}")
false
}
}
} else {
Timber.e("Unsupported message to resend ${event.type}")
Timber.e("Unsupported message to resend ${event.root.getClearType()}")
false
}
}
else -> {
Timber.e("Unsupported message to resend ${event.type}")
Timber.e("Unsupported message to resend ${event.root.getClearType()}")
false
}
}

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.room.send
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import javax.inject.Inject
/**
@ -29,11 +28,10 @@ import javax.inject.Inject
*/
internal class MarkdownParser @Inject constructor(
private val parser: Parser,
private val htmlRenderer: HtmlRenderer,
private val textContentRenderer: TextContentRenderer
private val htmlRenderer: HtmlRenderer
) {
private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex()
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
fun parse(text: String): TextContent {
// If no special char are detected, just return plain text
@ -54,8 +52,8 @@ internal class MarkdownParser @Inject constructor(
return if (isFormattedTextPertinent(text, cleanHtmlText)) {
// According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes:
// The plain text version of the HTML should be provided in the body.
val plainText = textContentRenderer.render(document)
TextContent(plainText, cleanHtmlText.postTreatment())
// But it caused too many problems so it has been removed in #2002
TextContent(text, cleanHtmlText.postTreatment())
} else {
TextContent(text)
}
@ -72,6 +70,7 @@ internal class MarkdownParser @Inject constructor(
// Remove extra space before and after the content
.trim()
// There is no need to include new line in an html-like source
.replace("\n", "")
// But new line can be in embedded code block, so do not remove them
// .replace("\n", "")
}
}

View file

@ -58,7 +58,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
@Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result {
Timber.v("Start dispatch sending multiple event work")
Timber.v("## SendEvent: Start dispatch sending multiple event work")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
@ -72,18 +72,21 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
}
// Transmit the error if needed?
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
.also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") }
}
// Create a work for every event
params.events.forEach { event ->
if (params.isEncrypted) {
Timber.v("Send event in encrypted room")
localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
val encryptWork = createEncryptEventWork(params.sessionId, event, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(params.sessionId, event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork)
} else {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
val sendWork = createSendEventWork(params.sessionId, event, true)
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork)
}

View file

@ -39,13 +39,16 @@ internal class RoomEventSender @Inject constructor(
) {
fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) {
Timber.v("Send event in encrypted room")
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")
&& !event.isEncrypted() // In case of resend where it's already encrypted so skip to send
) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
val encryptWork = createEncryptEventWork(event, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
} else {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
val sendWork = createSendEventWork(event, true)
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
}

View file

@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3
// private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3
/**
* Possible previous worker: [EncryptEventWorker] or first worker
@ -56,12 +56,12 @@ internal class SendEventWorker(context: Context,
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
.also { Timber.e("## SendEvent: Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
@ -75,22 +75,32 @@ internal class SendEventWorker(context: Context,
.also { Timber.e("Work cancelled due to bad input data") }
}
if (cancelSendTracker.isCancelRequestedFor(params.eventId, event.roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(event.eventId, event.roomId)
Timber.e("## SendEvent: Event sending has been cancelled ${params.eventId}")
}
}
if (params.lastFailureMessage != null) {
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
return try {
sendEvent(event.eventId, event.roomId, event.type, event.content)
Result.success()
} catch (exception: Throwable) {
// It does start from 0, we want it to stop if it fails the third time
val currentAttemptCount = runAttemptCount + 1
if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
return Result.success()
} else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
Result.retry()
}
}

View file

@ -115,6 +115,7 @@ internal class DefaultTimeline(
if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener
}
Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId")
handleUpdates(results, changeSet)
}

View file

@ -57,7 +57,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
}
}
fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(buildWorkName(roomId), policy, workRequest)
.enqueue()

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util
/**
* Base64 URL conversion methods
*/
internal fun base64UrlToBase64(base64Url: String): String {
return base64Url.replace('-', '+')
.replace('_', '/')
}
internal fun base64ToBase64Url(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')
.replace("=", "")
}
internal fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("=", "")
}

View file

@ -97,7 +97,7 @@ internal class DefaultGetWellknownTask @Inject constructor(
// Success
val homeServerBaseUrl = wellKnown.homeServer?.baseURL
if (homeServerBaseUrl.isNullOrBlank()) {
WellknownResult.FailPrompt
WellknownResult.FailPrompt(null, null)
} else {
if (homeServerBaseUrl.isValidUrl()) {
// Check that HS is a real one
@ -120,11 +120,11 @@ internal class DefaultGetWellknownTask @Inject constructor(
is Failure.OtherServerError -> {
when (throwable.httpCode) {
HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore
else -> WellknownResult.FailPrompt
else -> WellknownResult.FailPrompt(null, null)
}
}
is MalformedJsonException, is EOFException -> {
WellknownResult.FailPrompt
WellknownResult.FailPrompt(null, null)
}
else -> {
throw throwable
@ -162,7 +162,7 @@ internal class DefaultGetWellknownTask @Inject constructor(
// All is ok
WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown)
} else {
WellknownResult.FailError
WellknownResult.FailPrompt(homeServerBaseUrl, wellKnown)
}
} else {
WellknownResult.FailError

View file

@ -79,72 +79,6 @@
<string name="room_displayname_empty_room">Boş otaq</string>
<string name="verification_emoji_dog">It</string>
<string name="verification_emoji_cat">Pişik</string>
<string name="verification_emoji_lion">Aslan</string>
<string name="verification_emoji_horse">At</string>
<string name="verification_emoji_unicorn">Kərgədan</string>
<string name="verification_emoji_pig">Donuz</string>
<string name="verification_emoji_elephant">Fil</string>
<string name="verification_emoji_rabbit">Dovşan</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Xoruz</string>
<string name="verification_emoji_penguin">Pinqvin</string>
<string name="verification_emoji_turtle">Tısbağa</string>
<string name="verification_emoji_fish">Balıq</string>
<string name="verification_emoji_octopus">Ahtapot</string>
<string name="verification_emoji_butterfly">Kəpənək</string>
<string name="verification_emoji_flower">Çiçək</string>
<string name="verification_emoji_tree">Ağac</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Göbələk</string>
<string name="verification_emoji_globe">Qlobus</string>
<string name="verification_emoji_moon">Ay</string>
<string name="verification_emoji_cloud">Bulud</string>
<string name="verification_emoji_fire">Atəş</string>
<string name="verification_emoji_banana">Banan</string>
<string name="verification_emoji_apple">Alma</string>
<string name="verification_emoji_strawberry">Çiyələk</string>
<string name="verification_emoji_corn">Qarğıdalı</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Tort</string>
<string name="verification_emoji_heart">Ürək</string>
<string name="verification_emoji_smiley">Təbəssüm</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Papaq</string>
<string name="verification_emoji_glasses">Eynəklər</string>
<string name="verification_emoji_wrench">Açar</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Baş barmaqlar yuxarı</string>
<string name="verification_emoji_umbrella">Çətir</string>
<string name="verification_emoji_hourglass">Qum saatı</string>
<string name="verification_emoji_clock">Saat</string>
<string name="verification_emoji_gift">Hədiyyə</string>
<string name="verification_emoji_lightbulb">Lampa</string>
<string name="verification_emoji_book">Kitab</string>
<string name="verification_emoji_pencil">Qələm</string>
<string name="verification_emoji_paperclip">Kağız sancağı</string>
<string name="verification_emoji_scissors">Qayçı</string>
<string name="verification_emoji_lock">Qıfıl</string>
<string name="verification_emoji_key">Açar</string>
<string name="verification_emoji_hammer">Çəkic</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Bayraq</string>
<string name="verification_emoji_train">Qatar</string>
<string name="verification_emoji_bicycle">Velosiped</string>
<string name="verification_emoji_airplane">Təyyarə</string>
<string name="verification_emoji_rocket">Raket</string>
<string name="verification_emoji_trophy">Kubok</string>
<string name="verification_emoji_ball">Top</string>
<string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Saz</string>
<string name="verification_emoji_bell">Zəng</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphone">Qulaqlıqlar</string>
<string name="verification_emoji_folder">Qovluq</string>
<string name="verification_emoji_pin">Sancaq</string>
<string name="initial_sync_start_importing_account">İlkin sinxronizasiya:
\nHesab idxal olunur…</string>
<string name="initial_sync_start_importing_account_crypto">İlkin sinxronizasiya:

View file

@ -78,70 +78,6 @@
<string name="notice_event_redacted_by">Съобщение премахнато от %1$s</string>
<string name="notice_event_redacted_with_reason">Премахнато съобщение [причина: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string>
<string name="verification_emoji_dog">Куче</string>
<string name="verification_emoji_cat">Котка</string>
<string name="verification_emoji_lion">Лъв</string>
<string name="verification_emoji_horse">Кон</string>
<string name="verification_emoji_unicorn">Еднорог</string>
<string name="verification_emoji_pig">Прасе</string>
<string name="verification_emoji_elephant">Слон</string>
<string name="verification_emoji_rabbit">Заек</string>
<string name="verification_emoji_panda">Панда</string>
<string name="verification_emoji_rooster">Петел</string>
<string name="verification_emoji_penguin">Пингвин</string>
<string name="verification_emoji_turtle">Костенурка</string>
<string name="verification_emoji_fish">Риба</string>
<string name="verification_emoji_octopus">Октопод</string>
<string name="verification_emoji_butterfly">Пеперуда</string>
<string name="verification_emoji_flower">Цвете</string>
<string name="verification_emoji_tree">Дърво</string>
<string name="verification_emoji_cactus">Кактус</string>
<string name="verification_emoji_mushroom">Гъба</string>
<string name="verification_emoji_globe">Глобус</string>
<string name="verification_emoji_moon">Луна</string>
<string name="verification_emoji_cloud">Облак</string>
<string name="verification_emoji_fire">Огън</string>
<string name="verification_emoji_banana">Банан</string>
<string name="verification_emoji_apple">Ябълка</string>
<string name="verification_emoji_strawberry">Ягода</string>
<string name="verification_emoji_corn">Царевица</string>
<string name="verification_emoji_pizza">Пица</string>
<string name="verification_emoji_cake">Торта</string>
<string name="verification_emoji_heart">Сърце</string>
<string name="verification_emoji_smiley">Усмивка</string>
<string name="verification_emoji_robot">Робот</string>
<string name="verification_emoji_hat">Шапка</string>
<string name="verification_emoji_glasses">Очила</string>
<string name="verification_emoji_wrench">Гаечен ключ</string>
<string name="verification_emoji_santa">Дядо Коледа</string>
<string name="verification_emoji_thumbsup">Палец нагоре</string>
<string name="verification_emoji_umbrella">Чадър</string>
<string name="verification_emoji_hourglass">Пясъчен часовник</string>
<string name="verification_emoji_clock">Часовник</string>
<string name="verification_emoji_gift">Подарък</string>
<string name="verification_emoji_lightbulb">Лампа</string>
<string name="verification_emoji_book">Книга</string>
<string name="verification_emoji_pencil">Молив</string>
<string name="verification_emoji_paperclip">Кламер</string>
<string name="verification_emoji_scissors">Ножици</string>
<string name="verification_emoji_lock">Катинар</string>
<string name="verification_emoji_key">Ключ</string>
<string name="verification_emoji_hammer">Чук</string>
<string name="verification_emoji_telephone">Телефон</string>
<string name="verification_emoji_flag">Знаме</string>
<string name="verification_emoji_train">Влак</string>
<string name="verification_emoji_bicycle">Колело</string>
<string name="verification_emoji_airplane">Самолет</string>
<string name="verification_emoji_rocket">Ракета</string>
<string name="verification_emoji_trophy">Трофей</string>
<string name="verification_emoji_ball">Топка</string>
<string name="verification_emoji_guitar">Китара</string>
<string name="verification_emoji_trumpet">Тромпет</string>
<string name="verification_emoji_bell">Звънец</string>
<string name="verification_emoji_anchor">Котва</string>
<string name="verification_emoji_headphone">Слушалки</string>
<string name="verification_emoji_folder">Папка</string>
<string name="verification_emoji_pin">Карфица</string>
<string name="initial_sync_start_importing_account">Начална синхронизация:
\nИмпортиране на профил…</string>
@ -204,4 +140,16 @@
<string name="key_verification_request_fallback_message">%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове.</string>
<string name="notice_room_created">%1$s създаде стаята</string>
<string name="summary_you_sent_image">Изпратихте снимка.</string>
<string name="summary_you_sent_sticker">Изпратихте стикер.</string>
<string name="notice_room_invite_no_invitee_by_you">Ваша покана</string>
<string name="notice_room_created_by_you">Създадохте стаята</string>
<string name="notice_room_invite_by_you">Поканихте %1$s</string>
<string name="notice_room_join_by_you">Присъединихте се в стаята</string>
<string name="notice_room_leave_by_you">Напуснахте стаята</string>
<string name="notice_room_reject_by_you">Отхвърлихте поканата</string>
<string name="notice_room_kick_by_you">Изгонихте %1$s</string>
<string name="notice_room_unban_by_you">Отблокирахте %1$s</string>
<string name="notice_room_ban_by_you">Блокирахте %1$s</string>
</resources>

View file

@ -136,72 +136,6 @@
<string name="room_displayname_empty_room">খালি কক্ষ</string>
<string name="verification_emoji_dog">কুকুর</string>
<string name="verification_emoji_cat">বেড়াল</string>
<string name="verification_emoji_lion">সিংহ</string>
<string name="verification_emoji_horse">ঘোড়া</string>
<string name="verification_emoji_unicorn">ইউনিকর্ন</string>
<string name="verification_emoji_pig">শূকর</string>
<string name="verification_emoji_elephant">হাতি</string>
<string name="verification_emoji_rabbit">খরগোশ</string>
<string name="verification_emoji_panda">পান্ডা</string>
<string name="verification_emoji_rooster">গৃহপালিত মোরগ</string>
<string name="verification_emoji_penguin">পেংগুইন</string>
<string name="verification_emoji_turtle">কচ্ছপ</string>
<string name="verification_emoji_fish">মাছ</string>
<string name="verification_emoji_octopus">অক্টোপাস</string>
<string name="verification_emoji_butterfly">প্রজাপতি</string>
<string name="verification_emoji_flower">ফুল</string>
<string name="verification_emoji_tree">গাছ</string>
<string name="verification_emoji_cactus">ফণীমনসা</string>
<string name="verification_emoji_mushroom">মাশরুম</string>
<string name="verification_emoji_globe">পৃথিবী</string>
<string name="verification_emoji_moon">চন্দ্র</string>
<string name="verification_emoji_cloud">মেঘ</string>
<string name="verification_emoji_fire">আগুন</string>
<string name="verification_emoji_banana">কলা</string>
<string name="verification_emoji_apple">আপেল</string>
<string name="verification_emoji_strawberry">স্ট্রবেরি</string>
<string name="verification_emoji_corn">ভূট্টা</string>
<string name="verification_emoji_pizza">পিজা</string>
<string name="verification_emoji_cake">কেক</string>
<string name="verification_emoji_heart">হৃদয়</string>
<string name="verification_emoji_smiley">স্মাইলি</string>
<string name="verification_emoji_robot">রোবট</string>
<string name="verification_emoji_hat">টুপি</string>
<string name="verification_emoji_glasses">চশমা</string>
<string name="verification_emoji_wrench">রেঞ্চ</string>
<string name="verification_emoji_santa">সান্তা</string>
<string name="verification_emoji_thumbsup">থাম্বস আপ</string>
<string name="verification_emoji_umbrella">ছাতা</string>
<string name="verification_emoji_hourglass">বালিঘড়ি</string>
<string name="verification_emoji_clock">ঘড়ি</string>
<string name="verification_emoji_gift">উপহার</string>
<string name="verification_emoji_lightbulb">আলো বালব</string>
<string name="verification_emoji_book">বই</string>
<string name="verification_emoji_pencil">পেন্সিল</string>
<string name="verification_emoji_paperclip">পেপার ক্লিপ</string>
<string name="verification_emoji_scissors">কাঁচি</string>
<string name="verification_emoji_lock">লক</string>
<string name="verification_emoji_key">চাবি</string>
<string name="verification_emoji_hammer">হাতুড়ি</string>
<string name="verification_emoji_telephone">টেলিফোন</string>
<string name="verification_emoji_flag">পতাকা</string>
<string name="verification_emoji_train">রেলগাড়ি</string>
<string name="verification_emoji_bicycle">সাইকেল</string>
<string name="verification_emoji_airplane">বিমান</string>
<string name="verification_emoji_rocket">রকেট</string>
<string name="verification_emoji_trophy">ট্রফি</string>
<string name="verification_emoji_ball">বল</string>
<string name="verification_emoji_guitar">গিটার</string>
<string name="verification_emoji_trumpet">ট্রাম্পেট</string>
<string name="verification_emoji_bell">ঘণ্টা</string>
<string name="verification_emoji_anchor">নোঙ্গর</string>
<string name="verification_emoji_headphone">হেডফোন</string>
<string name="verification_emoji_folder">ফোল্ডার</string>
<string name="verification_emoji_pin">পিন</string>
<string name="initial_sync_start_importing_account">প্রাথমিক সিঙ্ক:
\nঅ্যাকাউন্ট আমদানি করা হচ্ছে…</string>
<string name="initial_sync_start_importing_account_crypto">প্রাথমিক সিঙ্ক:
@ -288,8 +222,4 @@
<string name="key_verification_request_fallback_message">%s আপনার কীটি যাচাই করার জন্য অনুরোধ করছে, তবে আপনার ক্লায়েন্ট ইন-চ্যাট কী যাচাইকরণ সমর্থন করে না। কীগুলি যাচাই করতে আপনাকে লিগ্যাসি কী যাচাইকরণ ব্যবহার করতে হবে।</string>
<string name="call_notification_answer">গ্রহণ</string>
<string name="call_notification_reject">পতন</string>
<string name="call_notification_hangup">বন্ধ করুন</string>
</resources>

View file

@ -79,68 +79,7 @@
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Zpráva smazána uživatelem %1$s [důvod: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">Uživatel %1$s obnovil pozvánku do místnosti pro uživatele %2$s</string>
<string name="verification_emoji_cat">Kočka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kůň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prase</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Králík</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohout</string>
<string name="verification_emoji_penguin">Tučňák</string>
<string name="verification_emoji_turtle">Želva</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnice</string>
<string name="verification_emoji_butterfly">Motýl</string>
<string name="verification_emoji_flower">Květina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Houba</string>
<string name="verification_emoji_globe">Zeměkoule</string>
<string name="verification_emoji_moon">Měsíc</string>
<string name="verification_emoji_cloud">Mrak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukuřice</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Dort</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Smajlík</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobouk</string>
<string name="verification_emoji_glasses">Brýle</string>
<string name="verification_emoji_santa">Santa Klaus</string>
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
<string name="verification_emoji_umbrella">Deštník</string>
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Dárek</string>
<string name="verification_emoji_lightbulb">Žárovka</string>
<string name="verification_emoji_book">Kniha</string>
<string name="verification_emoji_pencil">Tužka</string>
<string name="verification_emoji_paperclip">Sponka</string>
<string name="verification_emoji_scissors">Nůžky</string>
<string name="verification_emoji_lock">Zámek</string>
<string name="verification_emoji_key">Klíč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Jízdní kolo</string>
<string name="verification_emoji_airplane">Letadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Míč</string>
<string name="verification_emoji_guitar">Kytara</string>
<string name="verification_emoji_trumpet">Trumpeta</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Sluchátka</string>
<string name="verification_emoji_folder">Desky</string>
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
\nImport účtu…</string>
<string name="initial_sync_start_importing_account_crypto">Úvodní synchronizace:
\nImport klíčů</string>
@ -156,8 +95,6 @@
\nImport dat účtu</string>
<string name="event_status_sending_message">Odesílání zprávy…</string>
<string name="verification_emoji_wrench">Maticový klíč</string>
<string name="verification_emoji_pin">Připínáček</string>
<string name="initial_sync_start_importing_account_invited_rooms">Úvodní synchronizace:
\nImport pozvánek</string>

View file

@ -89,73 +89,8 @@
<string name="notice_event_redacted_by">Nachricht entfernt von %1$s</string>
<string name="notice_event_redacted_with_reason">Nachricht entfernt [Grund: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Nachricht entfernt von %1$s [Grund: %2$s]</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_dog">Hund</string>
<string name="verification_emoji_cat">Katze</string>
<string name="verification_emoji_lion">Löwe</string>
<string name="verification_emoji_horse">Pferd</string>
<string name="verification_emoji_unicorn">Einhorn</string>
<string name="verification_emoji_pig">Schwein</string>
<string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Kaninchen</string>
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hahn</string>
<string name="verification_emoji_penguin">Pinguin</string>
<string name="verification_emoji_turtle">Schildkröte</string>
<string name="verification_emoji_fish">Fisch</string>
<string name="verification_emoji_octopus">Oktopus</string>
<string name="verification_emoji_butterfly">Schmetterling</string>
<string name="verification_emoji_flower">Blume</string>
<string name="verification_emoji_tree">Baum</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Pilz</string>
<string name="verification_emoji_globe">Globus</string>
<string name="verification_emoji_moon">Mond</string>
<string name="verification_emoji_cloud">Wolke</string>
<string name="verification_emoji_fire">Feuer</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Apfel</string>
<string name="verification_emoji_strawberry">Erdbeere</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_cake">Kuchen</string>
<string name="verification_emoji_heart">Herz</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Roboter</string>
<string name="verification_emoji_hat">Hut</string>
<string name="verification_emoji_glasses">Brille</string>
<string name="verification_emoji_wrench">Schraubenschlüssel</string>
<string name="verification_emoji_santa">Weihnachtsmann</string>
<string name="verification_emoji_thumbsup">Daumen hoch</string>
<string name="verification_emoji_umbrella">Regenschirm</string>
<string name="verification_emoji_hourglass">Sanduhr</string>
<string name="verification_emoji_clock">Uhr</string>
<string name="verification_emoji_gift">Geschenk</string>
<string name="verification_emoji_lightbulb">Glühbirne</string>
<string name="verification_emoji_book">Buch</string>
<string name="verification_emoji_pencil">Bleistift</string>
<string name="verification_emoji_paperclip">Büroklammer</string>
<string name="verification_emoji_scissors">Schere</string>
<string name="verification_emoji_lock">Schloss</string>
<string name="verification_emoji_key">Schlüssel</string>
<string name="verification_emoji_hammer">Hammer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagge</string>
<string name="verification_emoji_train">Zug</string>
<string name="verification_emoji_bicycle">Fahrrad</string>
<string name="verification_emoji_airplane">Flugzeug</string>
<string name="verification_emoji_rocket">Rakete</string>
<string name="verification_emoji_trophy">Pokal</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitarre</string>
<string name="verification_emoji_trumpet">Trompete</string>
<string name="verification_emoji_bell">Glocke</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphone">Kopfhörer</string>
<string name="verification_emoji_folder">Ordner</string>
<string name="verification_emoji_pin">Stecknadel</string>
<string name="event_status_sending_message">Sende eine Nachricht…</string>
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
@ -297,10 +232,6 @@
<string name="notice_end_to_end_ok_by_you">Du hast Ende-zu-Ende-Verschlüsselung aktiviert.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Du hast Ende-zu-Ende-Verschlüsselung aktiviert (unbekannter Algorithmus %1$s).</string>
<string name="call_notification_answer">Akzeptiere</string>
<string name="call_notification_reject">Ablehnen</string>
<string name="call_notification_hangup">Anruf beenden</string>
<string name="notice_call_candidates">%s hat Daten gesendet, um einen Anruf zu starten.</string>
<string name="notice_call_candidates_by_you">Du hast Daten geschickt, um eine Anruf zu starten.</string>
</resources>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Hund</string>
<string name="verification_emoji_cat">Katze</string>
<string name="verification_emoji_lion">Löwe</string>
<string name="verification_emoji_horse">Pferd</string>
<string name="verification_emoji_unicorn">Einhorn</string>
<string name="verification_emoji_pig">Schwein</string>
<string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Hase</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hahn</string>
<string name="verification_emoji_penguin">Pinguin</string>
<string name="verification_emoji_turtle">Schildkröte</string>
<string name="verification_emoji_fish">Fisch</string>
<string name="verification_emoji_octopus">Oktopus</string>
<string name="verification_emoji_butterfly">Schmetterling</string>
<string name="verification_emoji_flower">Blume</string>
<string name="verification_emoji_tree">Baum</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Pilz</string>
<string name="verification_emoji_globe">Globus</string>
<string name="verification_emoji_moon">Mond</string>
<string name="verification_emoji_cloud">Wolke</string>
<string name="verification_emoji_fire">Feuer</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Apfel</string>
<string name="verification_emoji_strawberry">Erdbeere</string>
<string name="verification_emoji_corn">Korn</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Kuchen</string>
<string name="verification_emoji_heart">Herz</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Roboter</string>
<string name="verification_emoji_hat">Hut</string>
<string name="verification_emoji_glasses">Brille</string>
<string name="verification_emoji_spanner">Schraubenschlüssel</string>
<string name="verification_emoji_santa">Nikolaus</string>
<string name="verification_emoji_thumbs_up">Daumen Hoch</string>
<string name="verification_emoji_umbrella">Regenschirm</string>
<string name="verification_emoji_hourglass">Sanduhr</string>
<string name="verification_emoji_clock">Wecker</string>
<string name="verification_emoji_gift">Geschenk</string>
<string name="verification_emoji_light_bulb">Glühbirne</string>
<string name="verification_emoji_book">Buch</string>
<string name="verification_emoji_pencil">Bleistift</string>
<string name="verification_emoji_paperclip">Büroklammer</string>
<string name="verification_emoji_scissors">Schere</string>
<string name="verification_emoji_lock">Schloss</string>
<string name="verification_emoji_key">Schlüssel</string>
<string name="verification_emoji_hammer">Hammer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagge</string>
<string name="verification_emoji_train">Zug</string>
<string name="verification_emoji_bicycle">Fahrrad</string>
<string name="verification_emoji_aeroplane">Flugzeug</string>
<string name="verification_emoji_rocket">Rakete</string>
<string name="verification_emoji_trophy">Trophäe</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitarre</string>
<string name="verification_emoji_trumpet">Trompete</string>
<string name="verification_emoji_bell">Glocke</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphones">Kopfhörer</string>
<string name="verification_emoji_folder">Ordner</string>
<string name="verification_emoji_pin">Stecknadel</string>
</resources>

View file

@ -1,5 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="verification_emoji_wrench">Spanner</string>
<string name="verification_emoji_airplane">Aeroplane</string>
</resources>

View file

@ -72,72 +72,6 @@
<string name="room_displayname_empty_room">Malplena ĉambro</string>
<string name="verification_emoji_dog">Hundo</string>
<string name="verification_emoji_cat">Kato</string>
<string name="verification_emoji_lion">Leono</string>
<string name="verification_emoji_horse">Ĉevalo</string>
<string name="verification_emoji_unicorn">Unukorno</string>
<string name="verification_emoji_pig">Porko</string>
<string name="verification_emoji_elephant">Elefanto</string>
<string name="verification_emoji_rabbit">Kuniklo</string>
<string name="verification_emoji_panda">Pando</string>
<string name="verification_emoji_rooster">Koko</string>
<string name="verification_emoji_penguin">Pingveno</string>
<string name="verification_emoji_turtle">Testudo</string>
<string name="verification_emoji_fish">Fiŝo</string>
<string name="verification_emoji_octopus">Polpo</string>
<string name="verification_emoji_butterfly">Papilio</string>
<string name="verification_emoji_flower">Floro</string>
<string name="verification_emoji_tree">Arbo</string>
<string name="verification_emoji_cactus">Kakto</string>
<string name="verification_emoji_mushroom">Fungo</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Luno</string>
<string name="verification_emoji_cloud">Nubo</string>
<string name="verification_emoji_fire">Fajro</string>
<string name="verification_emoji_banana">Banano</string>
<string name="verification_emoji_apple">Pomo</string>
<string name="verification_emoji_strawberry">Frago</string>
<string name="verification_emoji_corn">Maizo</string>
<string name="verification_emoji_pizza">Pico</string>
<string name="verification_emoji_cake">Kuko</string>
<string name="verification_emoji_heart">Koro</string>
<string name="verification_emoji_smiley">Mieneto</string>
<string name="verification_emoji_robot">Roboto</string>
<string name="verification_emoji_hat">Ĉapelo</string>
<string name="verification_emoji_glasses">Okulvitroj</string>
<string name="verification_emoji_wrench">Boltilo</string>
<string name="verification_emoji_santa">Kristnaska viro</string>
<string name="verification_emoji_thumbsup">Dikfingro supren</string>
<string name="verification_emoji_umbrella">Ombrelo</string>
<string name="verification_emoji_hourglass">Sablohorloĝo</string>
<string name="verification_emoji_clock">Horloĝo</string>
<string name="verification_emoji_gift">Donaco</string>
<string name="verification_emoji_lightbulb">Lampo</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Grifelo</string>
<string name="verification_emoji_paperclip">Paperkuntenilo</string>
<string name="verification_emoji_scissors">Tondilo</string>
<string name="verification_emoji_lock">Seruro</string>
<string name="verification_emoji_key">Ŝlosilo</string>
<string name="verification_emoji_hammer">Martelo</string>
<string name="verification_emoji_telephone">Telefono</string>
<string name="verification_emoji_flag">Flago</string>
<string name="verification_emoji_train">Vagonaro</string>
<string name="verification_emoji_bicycle">Biciklo</string>
<string name="verification_emoji_airplane">Aviadilo</string>
<string name="verification_emoji_rocket">Raketo</string>
<string name="verification_emoji_trophy">Trofeo</string>
<string name="verification_emoji_ball">Pilko</string>
<string name="verification_emoji_guitar">Gitaro</string>
<string name="verification_emoji_trumpet">Trumpeto</string>
<string name="verification_emoji_bell">Sonorilo</string>
<string name="verification_emoji_anchor">Ankro</string>
<string name="verification_emoji_headphone">Kapaŭdilo</string>
<string name="verification_emoji_folder">Dosierujo</string>
<string name="verification_emoji_pin">Pinglo</string>
<string name="initial_sync_start_importing_account">Komenca spegulado:
\nEnportante konton…</string>
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Hundo</string>
<string name="verification_emoji_cat">Kato</string>
<string name="verification_emoji_lion">Leono</string>
<string name="verification_emoji_horse">Ĉevalo</string>
<string name="verification_emoji_unicorn">Unukorno</string>
<string name="verification_emoji_pig">Porko</string>
<string name="verification_emoji_elephant">Elefanto</string>
<string name="verification_emoji_rabbit">Kuniklo</string>
<string name="verification_emoji_panda">Pando</string>
<string name="verification_emoji_rooster">Virkoko</string>
<string name="verification_emoji_penguin">Pingveno</string>
<string name="verification_emoji_turtle">Testudo</string>
<string name="verification_emoji_fish">Fiŝo</string>
<string name="verification_emoji_octopus">Polpo</string>
<string name="verification_emoji_butterfly">Papilio</string>
<string name="verification_emoji_flower">Floro</string>
<string name="verification_emoji_tree">Arbo</string>
<string name="verification_emoji_cactus">Kakto</string>
<string name="verification_emoji_mushroom">Fungo</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Luno</string>
<string name="verification_emoji_cloud">Nubo</string>
<string name="verification_emoji_fire">Fajro</string>
<string name="verification_emoji_banana">Banano</string>
<string name="verification_emoji_apple">Pomo</string>
<string name="verification_emoji_strawberry">Frago</string>
<string name="verification_emoji_corn">Maizo</string>
<string name="verification_emoji_pizza">Pico</string>
<string name="verification_emoji_cake">Torto</string>
<string name="verification_emoji_heart">Koro</string>
<string name="verification_emoji_smiley">Rideto</string>
<string name="verification_emoji_robot">Roboto</string>
<string name="verification_emoji_hat">Ĉapelo</string>
<string name="verification_emoji_glasses">Okulvitroj</string>
<string name="verification_emoji_spanner">Ŝraŭbŝlosilo</string>
<string name="verification_emoji_santa">Kristnaska viro</string>
<string name="verification_emoji_thumbs_up">Dikfingro supren</string>
<string name="verification_emoji_umbrella">Ombrelo</string>
<string name="verification_emoji_hourglass">Sablohorloĝo</string>
<string name="verification_emoji_clock">Horloĝo</string>
<string name="verification_emoji_gift">Donaco</string>
<string name="verification_emoji_light_bulb">Lampo</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Krajono</string>
<string name="verification_emoji_paperclip">Paperkuntenilo</string>
<string name="verification_emoji_scissors">Tondilo</string>
<string name="verification_emoji_lock">Seruro</string>
<string name="verification_emoji_key">Ŝlosilo</string>
<string name="verification_emoji_hammer">Martelo</string>
<string name="verification_emoji_telephone">Telefono</string>
<string name="verification_emoji_flag">Flago</string>
<string name="verification_emoji_train">Vagonaro</string>
<string name="verification_emoji_bicycle">Biciklo</string>
<string name="verification_emoji_aeroplane">Aviadilo</string>
<string name="verification_emoji_rocket">Raketo</string>
<string name="verification_emoji_trophy">Trofeo</string>
<string name="verification_emoji_ball">Pilko</string>
<string name="verification_emoji_guitar">Gitaro</string>
<string name="verification_emoji_trumpet">Trumpeto</string>
<string name="verification_emoji_bell">Sonorilo</string>
<string name="verification_emoji_anchor">Ankro</string>
<string name="verification_emoji_headphones">Kapaŭdilo</string>
<string name="verification_emoji_folder">Dosierujo</string>
<string name="verification_emoji_pin">Pinglo</string>
</resources>

View file

@ -89,8 +89,4 @@
<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>
<string name="notice_event_redacted_by_with_reason">Mensaje eliminado por %1$s [motivo: %2$s]</string>
<string name="verification_emoji_dog">Perro</string>
<string name="verification_emoji_cat">Gato</string>
<string name="verification_emoji_lion">León</string>
<string name="verification_emoji_horse">Caballo</string>
</resources>

View file

@ -90,65 +90,6 @@
<string name="notice_event_redacted_with_reason">Mensaje eliminado [motivo: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Mensaje eliminado por %1$s [motivo: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">%1$s ha revocado la invitación a unirse a la sala para %2$s</string>
<string name="verification_emoji_dog">Perro</string>
<string name="verification_emoji_cat">Gato</string>
<string name="verification_emoji_lion">León</string>
<string name="verification_emoji_horse">Caballo</string>
<string name="verification_emoji_unicorn">Unicornio</string>
<string name="verification_emoji_pig">Cerdo</string>
<string name="verification_emoji_elephant">Elefante</string>
<string name="verification_emoji_rabbit">Conejo</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Gallo</string>
<string name="verification_emoji_penguin">Pingüino</string>
<string name="verification_emoji_turtle">Tortuga</string>
<string name="verification_emoji_fish">Pez</string>
<string name="verification_emoji_octopus">Pulpo</string>
<string name="verification_emoji_butterfly">Mariposa</string>
<string name="verification_emoji_flower">Flor</string>
<string name="verification_emoji_tree">Árbol</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Seta</string>
<string name="verification_emoji_moon">Luna</string>
<string name="verification_emoji_cloud">Nube</string>
<string name="verification_emoji_fire">Fuego</string>
<string name="verification_emoji_banana">Plátano</string>
<string name="verification_emoji_apple">Manzana</string>
<string name="verification_emoji_strawberry">Fresa</string>
<string name="verification_emoji_corn">Maíz</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Pastel</string>
<string name="verification_emoji_heart">Corazón</string>
<string name="verification_emoji_hat">Sombrero</string>
<string name="verification_emoji_glasses">Gafas</string>
<string name="verification_emoji_wrench">Llave inglesa</string>
<string name="verification_emoji_thumbsup">Pulgares arriba</string>
<string name="verification_emoji_umbrella">Paraguas</string>
<string name="verification_emoji_hourglass">Reloj de arena</string>
<string name="verification_emoji_clock">Reloj</string>
<string name="verification_emoji_gift">Regalo</string>
<string name="verification_emoji_lightbulb">Bombilla</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Lápiz</string>
<string name="verification_emoji_paperclip">Clip</string>
<string name="verification_emoji_scissors">Tijeras</string>
<string name="verification_emoji_lock">Candado</string>
<string name="verification_emoji_key">Llave</string>
<string name="verification_emoji_hammer">Martillo</string>
<string name="verification_emoji_telephone">Teléfono</string>
<string name="verification_emoji_flag">Bandera</string>
<string name="verification_emoji_train">Tren</string>
<string name="verification_emoji_bicycle">Bicicleta</string>
<string name="verification_emoji_airplane">Avión</string>
<string name="verification_emoji_rocket">Cohete</string>
<string name="verification_emoji_trophy">Trofeo</string>
<string name="verification_emoji_ball">Pelota</string>
<string name="verification_emoji_guitar">Guitarra</string>
<string name="verification_emoji_trumpet">Trompeta</string>
<string name="verification_emoji_bell">Campana</string>
<string name="verification_emoji_anchor">Ancla</string>
<string name="verification_emoji_headphone">Auriculares</string>
<string name="verification_emoji_folder">Carpeta</string>
<string name="initial_sync_start_importing_account">Sincronización Inicial
\nImportando cuenta…</string>
<string name="initial_sync_start_importing_account_rooms">Sincronización Inicial:
@ -173,12 +114,6 @@
<string name="notice_room_update">%s ha actualizado la sala.</string>
<string name="verification_emoji_globe">Globo Terráqueo</string>
<string name="verification_emoji_smiley">Cara sonriente</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_santa">Papá Noel</string>
<string name="verification_emoji_pin">Pin</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:

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Perro</string>
<string name="verification_emoji_cat">Gato</string>
<string name="verification_emoji_lion">León</string>
<string name="verification_emoji_horse">Caballo</string>
<string name="verification_emoji_unicorn">Unicornio</string>
<string name="verification_emoji_pig">Cerdo</string>
<string name="verification_emoji_elephant">Elefante</string>
<string name="verification_emoji_rabbit">Conejo</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Gallo</string>
<string name="verification_emoji_penguin">Pingüino</string>
<string name="verification_emoji_turtle">Tortuga</string>
<string name="verification_emoji_fish">Pez</string>
<string name="verification_emoji_octopus">Pulpo</string>
<string name="verification_emoji_butterfly">Mariposa</string>
<string name="verification_emoji_flower">Flor</string>
<string name="verification_emoji_tree">Árbol</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Seta</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Luna</string>
<string name="verification_emoji_cloud">Nube</string>
<string name="verification_emoji_fire">Fuego</string>
<string name="verification_emoji_banana">Plátano</string>
<string name="verification_emoji_apple">Manzana</string>
<string name="verification_emoji_strawberry">Fresa</string>
<string name="verification_emoji_corn">Maíz</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Tarta</string>
<string name="verification_emoji_heart">Corazón</string>
<string name="verification_emoji_smiley">Emoticono</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Sombrero</string>
<string name="verification_emoji_glasses">Gafas</string>
<string name="verification_emoji_spanner">Llave inglesa</string>
<string name="verification_emoji_clock">Reloj</string>
<string name="verification_emoji_gift">Regalo</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Lápiz</string>
<string name="verification_emoji_key">Llave</string>
<string name="verification_emoji_hammer">Martillo</string>
<string name="verification_emoji_telephone">Telefono</string>
<string name="verification_emoji_train">Tren</string>
<string name="verification_emoji_bicycle">Bicicleta</string>
<string name="verification_emoji_ball">Bola</string>
<string name="verification_emoji_guitar">Guitarra</string>
<string name="verification_emoji_trumpet">Trompeta</string>
<string name="verification_emoji_bell">Campana</string>
<string name="verification_emoji_pin">Alfiler</string>
</resources>

View file

@ -77,72 +77,6 @@
<string name="room_displayname_empty_room">Tühi jututuba</string>
<string name="verification_emoji_dog">Koer</string>
<string name="verification_emoji_cat">Kass</string>
<string name="verification_emoji_lion">Lõvi</string>
<string name="verification_emoji_horse">Hobune</string>
<string name="verification_emoji_unicorn">Ükssarvik</string>
<string name="verification_emoji_pig">Siga</string>
<string name="verification_emoji_elephant">Elevant</string>
<string name="verification_emoji_rabbit">Jänes</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kukk</string>
<string name="verification_emoji_penguin">Pingviin</string>
<string name="verification_emoji_turtle">Kilpkonn</string>
<string name="verification_emoji_fish">Kala</string>
<string name="verification_emoji_octopus">Kaheksajalg</string>
<string name="verification_emoji_butterfly">Liblikas</string>
<string name="verification_emoji_flower">Lill</string>
<string name="verification_emoji_tree">Puu</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Seen</string>
<string name="verification_emoji_globe">Maakera</string>
<string name="verification_emoji_moon">Kuu</string>
<string name="verification_emoji_cloud">Pilv</string>
<string name="verification_emoji_fire">Tuli</string>
<string name="verification_emoji_banana">Banaan</string>
<string name="verification_emoji_apple">Õun</string>
<string name="verification_emoji_strawberry">Maasikas</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pitsa</string>
<string name="verification_emoji_cake">Kook</string>
<string name="verification_emoji_heart">Süda</string>
<string name="verification_emoji_smiley">Smaili</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Kübar</string>
<string name="verification_emoji_glasses">Prillid</string>
<string name="verification_emoji_wrench">Mutrivõti</string>
<string name="verification_emoji_santa">Jõuluvana</string>
<string name="verification_emoji_thumbsup">Pöidlad püsti</string>
<string name="verification_emoji_umbrella">Vihmavari</string>
<string name="verification_emoji_hourglass">Liivakell</string>
<string name="verification_emoji_clock">Kell</string>
<string name="verification_emoji_gift">Kingitus</string>
<string name="verification_emoji_lightbulb">Lambipirn</string>
<string name="verification_emoji_book">Raamat</string>
<string name="verification_emoji_pencil">Pliiats</string>
<string name="verification_emoji_paperclip">Kirjaklamber</string>
<string name="verification_emoji_scissors">Käärid</string>
<string name="verification_emoji_lock">Lukk</string>
<string name="verification_emoji_key">Võti</string>
<string name="verification_emoji_hammer">Haamer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Lipp</string>
<string name="verification_emoji_train">Rong</string>
<string name="verification_emoji_bicycle">Jalgratas</string>
<string name="verification_emoji_airplane">Lennuk</string>
<string name="verification_emoji_rocket">Rakett</string>
<string name="verification_emoji_trophy">Auhind</string>
<string name="verification_emoji_ball">Pall</string>
<string name="verification_emoji_guitar">Kitarr</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Kelluke</string>
<string name="verification_emoji_anchor">Ankur</string>
<string name="verification_emoji_headphone">Kõrvaklapid</string>
<string name="verification_emoji_folder">Kaust</string>
<string name="verification_emoji_pin">Nööpnõel</string>
<string name="initial_sync_start_importing_account">Alglaadimine:
\nImpordin kontot…</string>
<string name="initial_sync_start_importing_account_crypto">Alglaadimine:
@ -295,8 +229,4 @@
<string name="notice_end_to_end_ok_by_you">Sa lülitasid sisse läbiva krüptimise.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Sa lülitasid sisse läbiva krüptimise (kasutusel on tundmatu algoritm %1$s).</string>
<string name="call_notification_answer">Võta vastu</string>
<string name="call_notification_reject">Keeldu</string>
<string name="call_notification_hangup">Lõpeta kõne</string>
</resources>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Koer</string>
<string name="verification_emoji_cat">Kass</string>
<string name="verification_emoji_lion">Lõvi</string>
<string name="verification_emoji_horse">Hobune</string>
<string name="verification_emoji_unicorn">Ükssarvik</string>
<string name="verification_emoji_pig">Siga</string>
<string name="verification_emoji_elephant">Elevant</string>
<string name="verification_emoji_rabbit">Jänes</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kukk</string>
<string name="verification_emoji_penguin">Pingviin</string>
<string name="verification_emoji_turtle">Kilpkonn</string>
<string name="verification_emoji_fish">Kala</string>
<string name="verification_emoji_octopus">Kaheksajalg</string>
<string name="verification_emoji_butterfly">Liblikas</string>
<string name="verification_emoji_flower">Lill</string>
<string name="verification_emoji_tree">Puu</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Seen</string>
<string name="verification_emoji_globe">Maakera</string>
<string name="verification_emoji_moon">Kuu</string>
<string name="verification_emoji_cloud">Pilv</string>
<string name="verification_emoji_fire">Tuli</string>
<string name="verification_emoji_banana">Banaan</string>
<string name="verification_emoji_apple">Õun</string>
<string name="verification_emoji_strawberry">Maasikas</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pitsa</string>
<string name="verification_emoji_cake">Kook</string>
<string name="verification_emoji_heart">Süda</string>
<string name="verification_emoji_smiley">Smaili</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Kübar</string>
<string name="verification_emoji_glasses">Prillid</string>
<string name="verification_emoji_spanner">Mutrivõti</string>
<string name="verification_emoji_santa">Jõuluvana</string>
<string name="verification_emoji_thumbs_up">Pöidlad püsti</string>
<string name="verification_emoji_umbrella">Vihmavari</string>
<string name="verification_emoji_hourglass">Liivakell</string>
<string name="verification_emoji_clock">Kell</string>
<string name="verification_emoji_gift">Kingitus</string>
<string name="verification_emoji_light_bulb">Lambipirn</string>
<string name="verification_emoji_book">Raamat</string>
<string name="verification_emoji_pencil">Pliiats</string>
<string name="verification_emoji_paperclip">Kirjaklamber</string>
<string name="verification_emoji_scissors">Käärid</string>
<string name="verification_emoji_lock">Lukk</string>
<string name="verification_emoji_key">Võti</string>
<string name="verification_emoji_hammer">Haamer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Lipp</string>
<string name="verification_emoji_train">Rong</string>
<string name="verification_emoji_bicycle">Jalgratas</string>
<string name="verification_emoji_aeroplane">Lennuk</string>
<string name="verification_emoji_rocket">Rakett</string>
<string name="verification_emoji_trophy">Auhind</string>
<string name="verification_emoji_ball">Pall</string>
<string name="verification_emoji_guitar">Kitarr</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Kelluke</string>
<string name="verification_emoji_anchor">Ankur</string>
<string name="verification_emoji_headphones">Kõrvaklapid</string>
<string name="verification_emoji_folder">Kaust</string>
<string name="verification_emoji_pin">Nööpnõel</string>
</resources>

View file

@ -78,70 +78,6 @@
<string name="notice_event_redacted_by">%1$s erabiltzaileak mezua kendu du</string>
<string name="notice_event_redacted_with_reason">Mezua kendu da [arrazoia: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">%1$s erabiltzaileak mezua kendu du [arrazoia: %2$s]</string>
<string name="verification_emoji_dog">Txakurra</string>
<string name="verification_emoji_cat">Katua</string>
<string name="verification_emoji_lion">Lehoia</string>
<string name="verification_emoji_horse">Zaldia</string>
<string name="verification_emoji_unicorn">Unikornioa</string>
<string name="verification_emoji_pig">Zerria</string>
<string name="verification_emoji_elephant">Elefantea</string>
<string name="verification_emoji_rabbit">Untxia</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Oilarra</string>
<string name="verification_emoji_penguin">Pinguinoa</string>
<string name="verification_emoji_turtle">Dortoka</string>
<string name="verification_emoji_fish">Arraina</string>
<string name="verification_emoji_octopus">Olagarroa</string>
<string name="verification_emoji_butterfly">Tximeleta</string>
<string name="verification_emoji_flower">Lorea</string>
<string name="verification_emoji_tree">Zuhaitza</string>
<string name="verification_emoji_cactus">Kaktusa</string>
<string name="verification_emoji_mushroom">Perretxikoa</string>
<string name="verification_emoji_globe">Lurra</string>
<string name="verification_emoji_moon">Ilargia</string>
<string name="verification_emoji_cloud">Hodeia</string>
<string name="verification_emoji_fire">Sua</string>
<string name="verification_emoji_banana">Banana</string>
<string name="verification_emoji_apple">Sagarra</string>
<string name="verification_emoji_strawberry">Marrubia</string>
<string name="verification_emoji_corn">Artoa</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Pastela</string>
<string name="verification_emoji_heart">Bihotza</string>
<string name="verification_emoji_smiley">Irrifartxoa</string>
<string name="verification_emoji_robot">Robota</string>
<string name="verification_emoji_hat">Txanoa</string>
<string name="verification_emoji_glasses">Betaurrekoak</string>
<string name="verification_emoji_wrench">Giltza</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Ederto</string>
<string name="verification_emoji_umbrella">Aterkia</string>
<string name="verification_emoji_hourglass">Harea-erlojua</string>
<string name="verification_emoji_clock">Erlojua</string>
<string name="verification_emoji_gift">Oparia</string>
<string name="verification_emoji_lightbulb">Bonbilla</string>
<string name="verification_emoji_book">Liburua</string>
<string name="verification_emoji_pencil">Arkatza</string>
<string name="verification_emoji_paperclip">Klipa</string>
<string name="verification_emoji_scissors">Artaziak</string>
<string name="verification_emoji_lock">Giltzarrapoa</string>
<string name="verification_emoji_key">Giltza</string>
<string name="verification_emoji_hammer">Mailua</string>
<string name="verification_emoji_telephone">Telefonoa</string>
<string name="verification_emoji_flag">Bandera</string>
<string name="verification_emoji_train">Trena</string>
<string name="verification_emoji_bicycle">Bizikleta</string>
<string name="verification_emoji_airplane">Hegazkina</string>
<string name="verification_emoji_rocket">Kohetea</string>
<string name="verification_emoji_trophy">Saria</string>
<string name="verification_emoji_ball">Baloia</string>
<string name="verification_emoji_guitar">Gitarra</string>
<string name="verification_emoji_trumpet">Tronpeta</string>
<string name="verification_emoji_bell">Kanpaia</string>
<string name="verification_emoji_anchor">Aingura</string>
<string name="verification_emoji_headphone">Aurikularrak</string>
<string name="verification_emoji_folder">Karpeta</string>
<string name="verification_emoji_pin">Txintxeta</string>
<string name="initial_sync_start_importing_account">Hasierako sinkronizazioa:
\nKontua inportatzen…</string>

View file

@ -77,72 +77,6 @@
<string name="room_displayname_empty_room">اتاق خالی</string>
<string name="verification_emoji_dog">سگ</string>
<string name="verification_emoji_cat">گربه</string>
<string name="verification_emoji_lion">شیر</string>
<string name="verification_emoji_horse">اسب</string>
<string name="verification_emoji_unicorn">تک‌شاخ</string>
<string name="verification_emoji_pig">خوک</string>
<string name="verification_emoji_elephant">فیل</string>
<string name="verification_emoji_rabbit">خرگوش</string>
<string name="verification_emoji_panda">پاندا</string>
<string name="verification_emoji_rooster">خروس</string>
<string name="verification_emoji_penguin">پنگوئن</string>
<string name="verification_emoji_turtle">لاک‌پشت</string>
<string name="verification_emoji_fish">ماهی</string>
<string name="verification_emoji_octopus">هشت‌پا</string>
<string name="verification_emoji_butterfly">پروانه</string>
<string name="verification_emoji_flower">گل</string>
<string name="verification_emoji_tree">درخت</string>
<string name="verification_emoji_cactus">کاکتوس</string>
<string name="verification_emoji_mushroom">قارچ</string>
<string name="verification_emoji_globe">جهان</string>
<string name="verification_emoji_moon">ماه</string>
<string name="verification_emoji_cloud">ابر</string>
<string name="verification_emoji_fire">آتش</string>
<string name="verification_emoji_banana">موز</string>
<string name="verification_emoji_apple">سیب</string>
<string name="verification_emoji_strawberry">توت‌فرنگی</string>
<string name="verification_emoji_corn">بلال</string>
<string name="verification_emoji_pizza">پیتزا</string>
<string name="verification_emoji_cake">کیک</string>
<string name="verification_emoji_heart">قلب</string>
<string name="verification_emoji_smiley">لبخند</string>
<string name="verification_emoji_robot">آدم‌آهنی</string>
<string name="verification_emoji_hat">کلاه</string>
<string name="verification_emoji_glasses">عینک</string>
<string name="verification_emoji_wrench">آچار</string>
<string name="verification_emoji_santa">بابانوئل</string>
<string name="verification_emoji_thumbsup">شست</string>
<string name="verification_emoji_umbrella">چتر</string>
<string name="verification_emoji_hourglass">ساعت شنی</string>
<string name="verification_emoji_clock">ساعت</string>
<string name="verification_emoji_gift">هدیه</string>
<string name="verification_emoji_lightbulb">لامپ</string>
<string name="verification_emoji_book">کتاب</string>
<string name="verification_emoji_pencil">مداد</string>
<string name="verification_emoji_paperclip">گیره کاغذ</string>
<string name="verification_emoji_scissors">قیچی</string>
<string name="verification_emoji_lock">قفل</string>
<string name="verification_emoji_key">کلید</string>
<string name="verification_emoji_hammer">چکّش</string>
<string name="verification_emoji_telephone">تلفن</string>
<string name="verification_emoji_flag">پرچم</string>
<string name="verification_emoji_train">قطار</string>
<string name="verification_emoji_bicycle">دوچرخه</string>
<string name="verification_emoji_airplane">هواپیما</string>
<string name="verification_emoji_rocket">موشک</string>
<string name="verification_emoji_trophy">جام</string>
<string name="verification_emoji_ball">توپ</string>
<string name="verification_emoji_guitar">گیتار</string>
<string name="verification_emoji_trumpet">ترومپت</string>
<string name="verification_emoji_bell">زنگ</string>
<string name="verification_emoji_anchor">لنگر</string>
<string name="verification_emoji_headphone">هدفون</string>
<string name="verification_emoji_folder">پوشه</string>
<string name="verification_emoji_pin">پونز</string>
<string name="initial_sync_start_importing_account">همگام‌سازی نخستین:
\nدر حال درون‌ریزی حساب…</string>
<string name="initial_sync_start_importing_account_crypto">همگام‌سازی نخستین:

View file

@ -79,70 +79,6 @@
<string name="notice_event_redacted_by">%1$s poisti viestin</string>
<string name="notice_event_redacted_with_reason">Viesti poistettu [syy: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">%1$s poisti viestin [syy: %2$s]</string>
<string name="verification_emoji_dog">Koira</string>
<string name="verification_emoji_cat">Kissa</string>
<string name="verification_emoji_lion">Leijona</string>
<string name="verification_emoji_horse">Hevonen</string>
<string name="verification_emoji_unicorn">Yksisarvinen</string>
<string name="verification_emoji_pig">Sika</string>
<string name="verification_emoji_elephant">Norsu</string>
<string name="verification_emoji_rabbit">Kani</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kukko</string>
<string name="verification_emoji_penguin">Pingviini</string>
<string name="verification_emoji_turtle">Kilpikonna</string>
<string name="verification_emoji_fish">Kala</string>
<string name="verification_emoji_octopus">Tursas</string>
<string name="verification_emoji_butterfly">Perhonen</string>
<string name="verification_emoji_flower">Kukka</string>
<string name="verification_emoji_tree">Puu</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Sieni</string>
<string name="verification_emoji_globe">Maapallo</string>
<string name="verification_emoji_moon">Kuu</string>
<string name="verification_emoji_cloud">Pilvi</string>
<string name="verification_emoji_fire">Tuli</string>
<string name="verification_emoji_banana">Banaani</string>
<string name="verification_emoji_apple">Omena</string>
<string name="verification_emoji_strawberry">Mansikka</string>
<string name="verification_emoji_corn">Maissi</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Kakku</string>
<string name="verification_emoji_heart">Sydän</string>
<string name="verification_emoji_smiley">Hymiö</string>
<string name="verification_emoji_robot">Robotti</string>
<string name="verification_emoji_hat">Hattu</string>
<string name="verification_emoji_glasses">Silmälasit</string>
<string name="verification_emoji_wrench">Jakoavain</string>
<string name="verification_emoji_santa">Joulupukki</string>
<string name="verification_emoji_thumbsup">Peukut ylös</string>
<string name="verification_emoji_umbrella">Sateenvarjo</string>
<string name="verification_emoji_hourglass">Tiimalasi</string>
<string name="verification_emoji_clock">Kello</string>
<string name="verification_emoji_gift">Lahja</string>
<string name="verification_emoji_lightbulb">Hehkulamppu</string>
<string name="verification_emoji_book">Kirja</string>
<string name="verification_emoji_pencil">Lyijykynä</string>
<string name="verification_emoji_paperclip">Klemmari</string>
<string name="verification_emoji_scissors">Sakset</string>
<string name="verification_emoji_lock">Lukko</string>
<string name="verification_emoji_key">Avain</string>
<string name="verification_emoji_hammer">Vasara</string>
<string name="verification_emoji_telephone">Puhelin</string>
<string name="verification_emoji_flag">Lippu</string>
<string name="verification_emoji_train">Juna</string>
<string name="verification_emoji_bicycle">Polkupyörä</string>
<string name="verification_emoji_airplane">Lentokone</string>
<string name="verification_emoji_rocket">Raketti</string>
<string name="verification_emoji_trophy">Palkinto</string>
<string name="verification_emoji_ball">Pallo</string>
<string name="verification_emoji_guitar">Kitara</string>
<string name="verification_emoji_trumpet">Trumpetti</string>
<string name="verification_emoji_bell">Soittokello</string>
<string name="verification_emoji_anchor">Ankkuri</string>
<string name="verification_emoji_headphone">Kuulokkeet</string>
<string name="verification_emoji_folder">Kansio</string>
<string name="verification_emoji_pin">Nuppineula</string>
<string name="initial_sync_start_importing_account">Alkusynkronointi:
\nTuodaan tiliä…</string>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Koira</string>
<string name="verification_emoji_cat">Kissa</string>
<string name="verification_emoji_lion">Leijona</string>
<string name="verification_emoji_horse">Hevonen</string>
<string name="verification_emoji_unicorn">Yksisarvinen</string>
<string name="verification_emoji_pig">Sika</string>
<string name="verification_emoji_elephant">Norsu</string>
<string name="verification_emoji_rabbit">Kani</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kukko</string>
<string name="verification_emoji_penguin">Pingviini</string>
<string name="verification_emoji_turtle">Kilpikonna</string>
<string name="verification_emoji_fish">Kala</string>
<string name="verification_emoji_octopus">Tursas</string>
<string name="verification_emoji_butterfly">Perhonen</string>
<string name="verification_emoji_flower">Kukka</string>
<string name="verification_emoji_tree">Puu</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Sieni</string>
<string name="verification_emoji_globe">Maapallo</string>
<string name="verification_emoji_moon">Kuu</string>
<string name="verification_emoji_cloud">Pilvi</string>
<string name="verification_emoji_fire">Tuli</string>
<string name="verification_emoji_banana">Banaani</string>
<string name="verification_emoji_apple">Omena</string>
<string name="verification_emoji_strawberry">Mansikka</string>
<string name="verification_emoji_corn">Maissi</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Kakku</string>
<string name="verification_emoji_heart">Sydän</string>
<string name="verification_emoji_smiley">Hymynaama</string>
<string name="verification_emoji_robot">Robotti</string>
<string name="verification_emoji_hat">Hattu</string>
<string name="verification_emoji_glasses">Silmälasit</string>
<string name="verification_emoji_spanner">Mutteriavain</string>
<string name="verification_emoji_santa">Joulupukki</string>
<string name="verification_emoji_thumbs_up">Peukalo ylös</string>
<string name="verification_emoji_umbrella">Sateenvarjo</string>
<string name="verification_emoji_hourglass">Tiimalasi</string>
<string name="verification_emoji_clock">Pöytäkello</string>
<string name="verification_emoji_gift">Lahja</string>
<string name="verification_emoji_light_bulb">Hehkulamppu</string>
<string name="verification_emoji_book">Kirja</string>
<string name="verification_emoji_pencil">Lyijykynä</string>
<string name="verification_emoji_paperclip">Paperiliitin</string>
<string name="verification_emoji_scissors">Sakset</string>
<string name="verification_emoji_lock">Lukko</string>
<string name="verification_emoji_key">Avain</string>
<string name="verification_emoji_hammer">Vasara</string>
<string name="verification_emoji_telephone">Puhelin</string>
<string name="verification_emoji_flag">Lippu</string>
<string name="verification_emoji_train">Juna</string>
<string name="verification_emoji_bicycle">Polkupyörä</string>
<string name="verification_emoji_aeroplane">Lentokone</string>
<string name="verification_emoji_rocket">Raketti</string>
<string name="verification_emoji_trophy">Palkinto</string>
<string name="verification_emoji_ball">Pallo</string>
<string name="verification_emoji_guitar">Kitara</string>
<string name="verification_emoji_trumpet">Trumpetti</string>
<string name="verification_emoji_bell">Soittokello</string>
<string name="verification_emoji_anchor">Ankkuri</string>
<string name="verification_emoji_headphones">Kuulokkeet</string>
<string name="verification_emoji_folder">Kansio</string>
<string name="verification_emoji_pin">Nuppineula</string>
</resources>

View file

@ -78,70 +78,6 @@
<string name="notice_event_redacted_by">Message supprimé par %1$s</string>
<string name="notice_event_redacted_with_reason">Message supprimé [motif : %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Message supprimé par %1$s [motif : %2$s]</string>
<string name="verification_emoji_dog">Chien</string>
<string name="verification_emoji_cat">Chat</string>
<string name="verification_emoji_lion">Lion</string>
<string name="verification_emoji_horse">Cheval</string>
<string name="verification_emoji_unicorn">Licorne</string>
<string name="verification_emoji_pig">Cochon</string>
<string name="verification_emoji_elephant">Éléphant</string>
<string name="verification_emoji_rabbit">Lapin</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Coq</string>
<string name="verification_emoji_penguin">Manchot</string>
<string name="verification_emoji_turtle">Tortue</string>
<string name="verification_emoji_fish">Poisson</string>
<string name="verification_emoji_octopus">Pieuvre</string>
<string name="verification_emoji_butterfly">Papillon</string>
<string name="verification_emoji_flower">Fleur</string>
<string name="verification_emoji_tree">Arbre</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Champignon</string>
<string name="verification_emoji_globe">Terre</string>
<string name="verification_emoji_moon">Lune</string>
<string name="verification_emoji_cloud">Nuage</string>
<string name="verification_emoji_fire">Feu</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Pomme</string>
<string name="verification_emoji_strawberry">Fraise</string>
<string name="verification_emoji_corn">Maïs</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Gâteau</string>
<string name="verification_emoji_heart">Cœur</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Chapeau</string>
<string name="verification_emoji_glasses">Lunettes</string>
<string name="verification_emoji_wrench">Clé plate</string>
<string name="verification_emoji_santa">Père Noël</string>
<string name="verification_emoji_thumbsup">Pouce levé</string>
<string name="verification_emoji_umbrella">Parapluie</string>
<string name="verification_emoji_hourglass">Sablier</string>
<string name="verification_emoji_clock">Horloge</string>
<string name="verification_emoji_gift">Cadeau</string>
<string name="verification_emoji_lightbulb">Ampoule</string>
<string name="verification_emoji_book">Livre</string>
<string name="verification_emoji_pencil">Crayon</string>
<string name="verification_emoji_paperclip">Trombone</string>
<string name="verification_emoji_scissors">Ciseaux</string>
<string name="verification_emoji_lock">Cadenas</string>
<string name="verification_emoji_key">Clé</string>
<string name="verification_emoji_hammer">Marteau</string>
<string name="verification_emoji_telephone">Téléphone</string>
<string name="verification_emoji_flag">Drapeau</string>
<string name="verification_emoji_train">Train</string>
<string name="verification_emoji_bicycle">Vélo</string>
<string name="verification_emoji_airplane">Avion</string>
<string name="verification_emoji_rocket">Fusée</string>
<string name="verification_emoji_trophy">Trophée</string>
<string name="verification_emoji_ball">Balle</string>
<string name="verification_emoji_guitar">Guitare</string>
<string name="verification_emoji_trumpet">Trompette</string>
<string name="verification_emoji_bell">Cloche</string>
<string name="verification_emoji_anchor">Ancre</string>
<string name="verification_emoji_headphone">Écouteurs</string>
<string name="verification_emoji_folder">Dossier</string>
<string name="verification_emoji_pin">Épingle</string>
<string name="initial_sync_start_importing_account">Synchronisation initiale :
\nImportation du compte…</string>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Chien</string>
<string name="verification_emoji_cat">Chat</string>
<string name="verification_emoji_lion">Lion</string>
<string name="verification_emoji_horse">Cheval</string>
<string name="verification_emoji_unicorn">Licorne</string>
<string name="verification_emoji_pig">Cochon</string>
<string name="verification_emoji_elephant">Éléphant</string>
<string name="verification_emoji_rabbit">Lapin</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Coq</string>
<string name="verification_emoji_penguin">Manchot</string>
<string name="verification_emoji_turtle">Tortue</string>
<string name="verification_emoji_fish">Poisson</string>
<string name="verification_emoji_octopus">Poulpe</string>
<string name="verification_emoji_butterfly">Papillon</string>
<string name="verification_emoji_flower">Fleur</string>
<string name="verification_emoji_tree">Arbre</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Champignon</string>
<string name="verification_emoji_globe">Globe</string>
<string name="verification_emoji_moon">Lune</string>
<string name="verification_emoji_cloud">Nuage</string>
<string name="verification_emoji_fire">Feu</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Pomme</string>
<string name="verification_emoji_strawberry">Fraise</string>
<string name="verification_emoji_corn">Maïs</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Gâteau</string>
<string name="verification_emoji_heart">Cœur</string>
<string name="verification_emoji_smiley">Sourire</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Châpeau</string>
<string name="verification_emoji_glasses">Lunettes</string>
<string name="verification_emoji_spanner">Clé à molette</string>
<string name="verification_emoji_santa">Père Noël</string>
<string name="verification_emoji_thumbs_up">Pouce en l\'air</string>
<string name="verification_emoji_umbrella">Parapluie</string>
<string name="verification_emoji_hourglass">Sablier</string>
<string name="verification_emoji_clock">Réveil</string>
<string name="verification_emoji_gift">Cadeau</string>
<string name="verification_emoji_light_bulb">Ampoule</string>
<string name="verification_emoji_book">Livre</string>
<string name="verification_emoji_pencil">Crayon</string>
<string name="verification_emoji_paperclip">Trombone</string>
<string name="verification_emoji_scissors">Ciseaux</string>
<string name="verification_emoji_lock">Cadenas</string>
<string name="verification_emoji_key">Clé</string>
<string name="verification_emoji_hammer">Marteau</string>
<string name="verification_emoji_telephone">Téléphone</string>
<string name="verification_emoji_flag">Drapeau</string>
<string name="verification_emoji_train">Train</string>
<string name="verification_emoji_bicycle">Vélo</string>
<string name="verification_emoji_aeroplane">Avion</string>
<string name="verification_emoji_rocket">Fusée</string>
<string name="verification_emoji_trophy">Trophée</string>
<string name="verification_emoji_ball">Ballon</string>
<string name="verification_emoji_guitar">Guitare</string>
<string name="verification_emoji_trumpet">Trompette</string>
<string name="verification_emoji_bell">Cloche</string>
<string name="verification_emoji_anchor">Ancre</string>
<string name="verification_emoji_headphones">Casque audio</string>
<string name="verification_emoji_folder">Dossier</string>
<string name="verification_emoji_pin">Punaise</string>
</resources>

View file

@ -77,70 +77,6 @@
<string name="notice_event_redacted_by">Üzenetet eltávolította: %1$s</string>
<string name="notice_event_redacted_with_reason">Üzenet eltávolítva [ok: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Üzenetet eltávolította: %1$s [ok: %2$s]</string>
<string name="verification_emoji_dog">Kutya</string>
<string name="verification_emoji_cat">Macska</string>
<string name="verification_emoji_lion">Oroszlán</string>
<string name="verification_emoji_horse"></string>
<string name="verification_emoji_unicorn">Egyszarvú</string>
<string name="verification_emoji_pig">Malac</string>
<string name="verification_emoji_elephant">Elefánt</string>
<string name="verification_emoji_rabbit">Nyúl</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kakas</string>
<string name="verification_emoji_penguin">Pingvin</string>
<string name="verification_emoji_turtle">Teknős</string>
<string name="verification_emoji_fish">Hal</string>
<string name="verification_emoji_octopus">Polip</string>
<string name="verification_emoji_butterfly">Pillangó</string>
<string name="verification_emoji_flower">Virág</string>
<string name="verification_emoji_tree">Fa</string>
<string name="verification_emoji_cactus">Kaktusz</string>
<string name="verification_emoji_mushroom">Gomba</string>
<string name="verification_emoji_globe">Föld</string>
<string name="verification_emoji_moon">Hold</string>
<string name="verification_emoji_cloud">Felhő</string>
<string name="verification_emoji_fire">Tűz</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Alma</string>
<string name="verification_emoji_strawberry">Eper</string>
<string name="verification_emoji_corn">Kukorica</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Süti</string>
<string name="verification_emoji_heart">Szív</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Kalap</string>
<string name="verification_emoji_glasses">Szemüveg</string>
<string name="verification_emoji_wrench">Csavarkulcs</string>
<string name="verification_emoji_santa">Télapó</string>
<string name="verification_emoji_thumbsup">Hüvelykujj fel</string>
<string name="verification_emoji_umbrella">Esernyő</string>
<string name="verification_emoji_hourglass">Homokóra</string>
<string name="verification_emoji_clock">Óra</string>
<string name="verification_emoji_gift">Ajándék</string>
<string name="verification_emoji_lightbulb">Égő</string>
<string name="verification_emoji_book">Könyv</string>
<string name="verification_emoji_pencil">Ceruza</string>
<string name="verification_emoji_paperclip">Gémkapocs</string>
<string name="verification_emoji_scissors">Olló</string>
<string name="verification_emoji_lock">Zár</string>
<string name="verification_emoji_key">Kulcs</string>
<string name="verification_emoji_hammer">Kalapács</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Zászló</string>
<string name="verification_emoji_train">Vonat</string>
<string name="verification_emoji_bicycle">Kerékpár</string>
<string name="verification_emoji_airplane">Repülő</string>
<string name="verification_emoji_rocket">Rakéta</string>
<string name="verification_emoji_trophy">Trófea</string>
<string name="verification_emoji_ball">Labda</string>
<string name="verification_emoji_guitar">Gitár</string>
<string name="verification_emoji_trumpet">Trombita</string>
<string name="verification_emoji_bell">Harang</string>
<string name="verification_emoji_anchor">Vasmacska</string>
<string name="verification_emoji_headphone">Fejhallgató</string>
<string name="verification_emoji_folder">Mappa</string>
<string name="verification_emoji_pin"></string>
<string name="initial_sync_start_importing_account">Induló szinkronizáció:
\nFiók betöltése…</string>

View file

@ -78,70 +78,6 @@
<string name="notice_event_redacted_by">Messaggio rimosso da %1$s</string>
<string name="notice_event_redacted_with_reason">Messaggio rimosso [motivo: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Messaggio rimosso da %1$s [motivo: %2$s]</string>
<string name="verification_emoji_dog">Cane</string>
<string name="verification_emoji_cat">Gatto</string>
<string name="verification_emoji_lion">Leone</string>
<string name="verification_emoji_horse">Cavallo</string>
<string name="verification_emoji_unicorn">Unicorno</string>
<string name="verification_emoji_pig">Maiale</string>
<string name="verification_emoji_elephant">Elefante</string>
<string name="verification_emoji_rabbit">Coniglio</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Gallo</string>
<string name="verification_emoji_penguin">Pinguino</string>
<string name="verification_emoji_turtle">Tartaruga</string>
<string name="verification_emoji_fish">Pesce</string>
<string name="verification_emoji_octopus">Piovra</string>
<string name="verification_emoji_butterfly">Farfalla</string>
<string name="verification_emoji_flower">Fiore</string>
<string name="verification_emoji_tree">Albero</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Fungo</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Luna</string>
<string name="verification_emoji_cloud">Nuvola</string>
<string name="verification_emoji_fire">Fuoco</string>
<string name="verification_emoji_banana">Banana</string>
<string name="verification_emoji_apple">Mela</string>
<string name="verification_emoji_strawberry">Fragola</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Torta</string>
<string name="verification_emoji_heart">Cuore</string>
<string name="verification_emoji_smiley">Sorriso</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Cappello</string>
<string name="verification_emoji_glasses">Occhiali</string>
<string name="verification_emoji_wrench">Chiave inglese</string>
<string name="verification_emoji_santa">Babbo Natale</string>
<string name="verification_emoji_thumbsup">Pollice in su</string>
<string name="verification_emoji_umbrella">Ombrello</string>
<string name="verification_emoji_hourglass">Clessidra</string>
<string name="verification_emoji_clock">Orologio</string>
<string name="verification_emoji_gift">Regalo</string>
<string name="verification_emoji_lightbulb">Lampadina</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Matita</string>
<string name="verification_emoji_paperclip">Graffetta</string>
<string name="verification_emoji_scissors">Forbici</string>
<string name="verification_emoji_lock">Lucchetto</string>
<string name="verification_emoji_key">Chiave</string>
<string name="verification_emoji_hammer">Martello</string>
<string name="verification_emoji_telephone">Telefono</string>
<string name="verification_emoji_flag">Bandiera</string>
<string name="verification_emoji_train">Treno</string>
<string name="verification_emoji_bicycle">Bicicletta</string>
<string name="verification_emoji_airplane">Aeroplano</string>
<string name="verification_emoji_rocket">Razzo</string>
<string name="verification_emoji_trophy">Trofeo</string>
<string name="verification_emoji_ball">Palla</string>
<string name="verification_emoji_guitar">Chitarra</string>
<string name="verification_emoji_trumpet">Tromba</string>
<string name="verification_emoji_bell">Campana</string>
<string name="verification_emoji_anchor">Ancora</string>
<string name="verification_emoji_headphone">Cuffie</string>
<string name="verification_emoji_folder">Cartella</string>
<string name="verification_emoji_pin">Spillo</string>
<string name="initial_sync_start_importing_account">Sync iniziale:
\nImportazione account…</string>
@ -296,8 +232,4 @@
<string name="notice_end_to_end_ok_by_you">Hai attivato la crittografia end-to-end.</string>
<string name="notice_end_to_end_unknown_algorithm_by_you">Hai attivato la crittografia end-to-end (algoritmo %1$s sconosciuto).</string>
<string name="call_notification_answer">Accetta</string>
<string name="call_notification_reject">Rifiuta</string>
<string name="call_notification_hangup">Riaggancia</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog"></string>
<string name="verification_emoji_cat"></string>
<string name="verification_emoji_horse"></string>
<string name="verification_emoji_octopus">たこ</string>
<string name="verification_emoji_flower"></string>
<string name="verification_emoji_tree"></string>
<string name="verification_emoji_mushroom">きのこ</string>
<string name="verification_emoji_moon"></string>
<string name="verification_emoji_apple">リンゴ</string>
<string name="verification_emoji_cake">ケーキ</string>
<string name="verification_emoji_robot">ロボと</string>
<string name="verification_emoji_glasses">めがね</string>
<string name="verification_emoji_book"></string>
<string name="verification_emoji_telephone">電話機</string>
<string name="verification_emoji_train">電車</string>
<string name="verification_emoji_bicycle">自転車</string>
</resources>

View file

@ -0,0 +1,225 @@
<?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 t.yuzen tugna.</string>
<string name="summary_you_sent_image">Tuzneḍ tugna.</string>
<string name="notice_room_invite_no_invitee">Tinubga n %s</string>
<string name="notice_room_invite_no_invitee_by_you">Tinubga-k•m</string>
<string name="notice_room_created">%1$s yesnulfa-d taxxamt</string>
<string name="notice_room_created_by_you">Tesnulfaḍ-d taxxamt-a</string>
<string name="notice_room_invite">%1$s inced-d %2$s</string>
<string name="notice_room_invite_by_you">Tnecdeḍ-d %1$s</string>
<string name="notice_room_invite_you">%1$s inced-ik-id</string>
<string name="notice_room_join">%1$s yedda ɣer texxamt</string>
<string name="notice_room_join_by_you">Teddiḍ ɣer texxamt</string>
<string name="notice_room_leave">%1$s yeǧǧa taxxamt</string>
<string name="notice_room_leave_by_you">Teǧǧiḍ taxxamt</string>
<string name="notice_room_reject">%1$s yugi/tugi tinubga</string>
<string name="notice_room_reject_by_you">Tufiḍ tinubga</string>
<string name="notice_room_kick">%1$s yessufeɣ %2$s</string>
<string name="notice_room_kick_by_you">Tessufɣeḍ %1$s</string>
<string name="notice_avatar_url_changed_by_you">Tbeddleḍ avatar-inek·inem</string>
<string name="power_level_admin">Anedbal</string>
<string name="power_level_moderator">Aseɣyad</string>
<string name="power_level_default">Amezwer</string>
<string name="power_level_custom_no_value">Sagen</string>
<string name="notice_power_level_diff">%1$s seg %2$s ɣer %3$s</string>
<string name="message_failed_to_upload">Tegguma ad d-tali tugna</string>
<string name="medium_email">Tansa n yimayl</string>
<string name="summary_user_sent_sticker">%1$s azen astiker.</string>
<string name="summary_you_sent_sticker">Tuzneḍ amenṭaḍ.</string>
<string name="notice_room_unban">%1$s yekkes agdal i %2$s</string>
<string name="notice_room_unban_by_you">Tekkseḍ agdal i %1$s</string>
<string name="notice_room_ban">%1$s igdel %2$s</string>
<string name="notice_room_ban_by_you">Tgedleḍ %1$s</string>
<string name="notice_room_withdraw">%1$s issefsex tinubga n %2$s</string>
<string name="notice_room_withdraw_by_you">Tesfesxeḍ tinubga n %1$s</string>
<string name="notice_avatar_url_changed">%1$s ibeddel avatar-is</string>
<string name="notice_display_name_set">%1$s isbadu isem-is i d-ittuseknen ɣer %2$s</string>
<string name="notice_display_name_set_by_you">Tesbaduḍ isem-ik•im i d-ittuseknen ɣer %1$s</string>
<string name="notice_display_name_changed_from">%1$s ibeddel isem-is i d-ittuseknen seg %2$s ɣer %3$s</string>
<string name="notice_display_name_changed_from_by_you">Tbeddleḍ isem-ik•im i d-ittuseknen seg %1$s ɣer %2$s</string>
<string name="notice_display_name_removed">%1$s yekkes isem-is i d-ittuseknen (yella %2$s)</string>
<string name="notice_display_name_removed_by_you">Tekkseḍ isem-ik·im yettwaskanen (d %1$s)</string>
<string name="notice_room_topic_changed">%1$S isnifel asentel s: %2$S</string>
<string name="notice_room_topic_changed_by_you">Tesnifleḍ asentel s: %2$S</string>
<string name="notice_room_avatar_changed">%1$s ibeddel avaṭar n texxamt</string>
<string name="notice_room_avatar_changed_by_you">Tbeddleḍ avaṭar n texxamt</string>
<string name="notice_room_name_changed">%1$s ibeddel isem n texxamt s: %2$s</string>
<string name="notice_room_name_changed_by_you">Tbeddleḍ isem n texxamt s: %2$s</string>
<string name="notice_placed_video_call">%s isɛedda siwel s tvidyut.</string>
<string name="notice_placed_video_call_by_you">Tesɛeddaḍ siwel s tvidyut.</string>
<string name="notice_placed_voice_call">%s isɛedda asiwel s taɣect.</string>
<string name="notice_placed_voice_call_by_you">Tesɛeddaḍ siwel s taɣect.</string>
<string name="notice_call_candidates">%s yuzen isefka i usbadu n usiwel.</string>
<string name="notice_call_candidates_by_you">Tuzneḍ isefka i usbadu n usiwel.</string>
<string name="notice_answered_call">%s yerra ɣef usiwel.</string>
<string name="notice_answered_call_by_you">Terriḍ ɣef usiwel.</string>
<string name="notice_ended_call">%s iḥbes asiwel.</string>
<string name="notice_ended_call_by_you">Tḥebseḍ asiwel.</string>
<string name="notice_room_visibility_invited">meṛṛa iɛeggalen n texxamt, segmi ara d-ttwanecden.</string>
<string name="notice_room_visibility_joined">meṛṛa iɛeggalen n texamt, segmi ara d-rnun.</string>
<string name="notice_room_visibility_shared">meṛṛa iɛeggalen n texxamt.</string>
<string name="notice_room_visibility_world_readable">yal yiwen.</string>
<string name="notice_room_visibility_unknown">arussin (%s).</string>
<string name="notice_end_to_end">%1$s isermed awgelhen seg yixef ɣer yixef (%2$s)</string>
<string name="notice_end_to_end_by_you">Tesremdeḍ awgelhen seg yixef ɣer yixef (%2$s)</string>
<string name="notice_room_update">%s ileqqem taxxamt-a.</string>
<string name="notice_room_update_by_you">Tleqqmeḍ taxxamt-a.</string>
<string name="notice_requested_voip_conference">%1$s isuter-d asarag VoIP</string>
<string name="notice_requested_voip_conference_by_you">Tsutreḍ-d asarag VoIP</string>
<string name="notice_voip_started">Asarag VoIP yebda</string>
<string name="notice_voip_finished">Asarag VoIP yekfa</string>
<string name="notice_avatar_changed_too">(avatar daɣen ibeddel)</string>
<string name="notice_room_name_removed">%1$s yekkes isem n texxamt</string>
<string name="notice_room_name_removed_by_you">Tekkseḍ isem n texxamt</string>
<string name="notice_room_topic_removed">%1$s yekkes asentel n texxamt</string>
<string name="notice_room_topic_removed_by_you">Tekkseḍ asentel n texxamt</string>
<string name="notice_room_avatar_removed">%1$s yekkes avatar n texxamt</string>
<string name="notice_room_avatar_removed_by_you">Tekkseḍ avatar n texxamt</string>
<string name="notice_event_redacted">Izen ittwakkes</string>
<string name="notice_event_redacted_by">Izen ittwakkes sɣur %1$s</string>
<string name="notice_event_redacted_with_reason">Izen ittwakkes [tamentilt: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Izen ittwakkes sɣur %1$s [tamentilt: %2$s]</string>
<string name="notice_profile_change_redacted">%1$s ileqqem amaɣnu-ines %2$s</string>
<string name="notice_profile_change_redacted_by_you">Tleqqmeḍ amaɣnu-inek•inem %1$s</string>
<string name="notice_room_third_party_invite">%1$s yuzen tinubga i %2$s akken ad yeddu ɣer texxamt</string>
<string name="notice_room_third_party_invite_by_you">Tuzneḍ tinubga i %1$s akken ad yeddu ɣer texxamt</string>
<string name="notice_room_third_party_registered_invite">%1$s iqbel tinubga i %2$s</string>
<string name="notice_room_third_party_registered_invite_by_you">Tqebleḍ tinubga i %1$s</string>
<string name="notice_widget_added">%1$s yerna awiǧit %2$s</string>
<string name="notice_widget_added_by_you">Terniḍ awiǧit %1$s</string>
<string name="notice_widget_removed">%1$s yekkes awiǧit %2$s</string>
<string name="notice_widget_removed_by_you">Tekkseḍ awiǧit %1$s</string>
<string name="notice_widget_modified">%1$s ibeddel awiǧit %2$s</string>
<string name="notice_widget_modified_by_you">Tbeddleḍ awiǧit %1$s</string>
<string name="power_level_custom">Sagen (%1$)</string>
<string name="notice_power_level_changed_by_you">Tbeddleḍ aswir n tezmert n %1$s.</string>
<string name="notice_power_level_changed">%1$s ibeddel aswir n tezmert n %2$s.</string>
<string name="notice_crypto_unable_to_decrypt">** Awgelhen d awezɣi: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Ibenk n umazan ur aɣ-d-yuzin ara tisura i yizen-a.</string>
<string name="unable_to_send_message">Tuzna n yizen d tawezɣit</string>
<string name="network_error">Tuccḍa deg uẓeṭṭa</string>
<string name="matrix_error">Tuccḍa deg Matrix</string>
<string name="notice_made_future_room_visibility">%1$s iga amazray n texxamyt i d-iteddun yettban i %2$s</string>
<string name="notice_made_future_room_visibility_by_you">Tgiḍ amazray n texxamyt i d-iteddun yettban i %1$s</string>
<string name="notice_room_third_party_revoked_invite">%1$s issefsax tinubga i %2$s i wakken ad d-yekcem ɣer texxamt</string>
<string name="notice_room_third_party_revoked_invite_by_you">Tesfesxeḍ tinubga i %1$s i wakken ad d-yernu ɣer texxamt</string>
<string name="room_error_join_failed_empty_room">D awezɣi tura ad nales ad nuɣal ɣer texxamt tilemt.</string>
<string name="encrypted_message">Izen yettwawgelhen</string>
<string name="medium_phone_number">Uṭṭun n tiliɣri</string>
<string name="room_displayname_invite_from">Tinubga sɣur %s</string>
<string name="room_displayname_room_invite">Tinubga ɣer texxamt</string>
<string name="room_displayname_two_members">%1$s d %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s d 1 wayeḍ</item>
<item quantity="other">%1$s d %2$d wiyaḍ</item>
</plurals>
<string name="notice_end_to_end_unknown_algorithm_by_you">Tremdeḍ awgelhen seg yixef ɣer yixef (alguritm %1$s ur yettwassen ara).</string>
<string name="key_verification_request_fallback_message">%s isuter-d ad isenqed tasarut-ik·im, maca amsaɣ-ik·im ur issefrak ara asenqed n tsura deg yidiwenniyen. Ilaq-ak·am useqdec asenqed iqdim n tsura i usenqed n tsura.</string>
<string name="room_displayname_empty_room">Taxxamt tilemt</string>
<string name="initial_sync_start_importing_account">Amtawi n tazwara:
\nAktar n umiḍan…</string>
<string name="initial_sync_start_importing_account_crypto">Amtawi n tazwara:
\nAktar n uwgelhen</string>
<string name="initial_sync_start_importing_account_rooms">Amtawi n tazwara:
\nAktar n texxamin</string>
<string name="initial_sync_start_importing_account_joined_rooms">Amtawi n tazwara:
\nAktar n texxamin iɣer terniḍ</string>
<string name="initial_sync_start_importing_account_invited_rooms">Amtawi n tazwara:
\nAktar n texxamin iɣer tettwanecdeḍ</string>
<string name="initial_sync_start_importing_account_left_rooms">Amtawi n tazwara:
\nAktar n texxamin i teǧǧiḍ</string>
<string name="initial_sync_start_importing_account_groups">Amtawi n tazwara:
\nAktar n tmezdagnutin</string>
<string name="initial_sync_start_importing_account_data">Amtawi n tazwara:
\nAktar n yisefka n umiḍan</string>
<string name="event_status_sending_message">Tuzzna n yizen…</string>
<string name="notice_room_invite_no_invitee_with_reason">Tinubga n %1$s. Tamentilt: %2$s</string>
<string name="notice_room_invite_no_invitee_with_reason_by_you">Tinubga-k•m. Tamentilt: %1$s</string>
<string name="notice_room_invite_with_reason">%1$s inced %2$s. Tamentilt: %3$s</string>
<string name="notice_room_invite_with_reason_by_you">Tnecdeḍ %1$s. Tamentilt: %2$s</string>
<string name="notice_room_invite_you_with_reason">%1$s inced-ik•ikem. Tamentilt: %2$s</string>
<string name="notice_room_join_with_reason">%1$s yedda ɣer texxamt. Tamentilt: %2$s</string>
<string name="notice_room_join_with_reason_by_you">Teddiḍ ɣer texxamt. Tamentilt: %1$s</string>
<string name="notice_room_leave_with_reason">%1$s yeǧǧa taxxamt. Tamentilt: %2$s</string>
<string name="notice_room_leave_with_reason_by_you">Teǧǧiḍ taxxamt. Tamentilt: %1$s</string>
<string name="notice_room_reject_with_reason">%1$s yugi tinubga. Tamentilt: %2$s</string>
<string name="notice_room_reject_with_reason_by_you">Tugiḍ tinubga. Tamentilt: %1$s</string>
<string name="notice_room_kick_with_reason">%1$s yessufeɣ %2$s. Tamentilt: %3$s</string>
<string name="notice_room_kick_with_reason_by_you">Tessufɣeḍ %1$s. Tamentilt: %2$s</string>
<string name="notice_room_unban_with_reason">%1$s yekkes agdal i %2$s. Tamentilt: %3$s</string>
<string name="notice_room_unban_with_reason_by_you">Tekkseḍ agdal i %1$s. Tamentilt: %2$s</string>
<string name="notice_room_ban_with_reason">%1$s igdel %2$s. Tamentilt: %3$s</string>
<string name="notice_room_ban_with_reason_by_you">Tgedleḍ %1$s. Tamentilt: %2$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s yuzen tinubga i %2$s akken ad yeddu ɣer texxamt. Tamentilt: %3$s</string>
<string name="notice_room_third_party_invite_with_reason_by_you">Tuzneḍ tinubga i %1$s iwakken ad yeddu ɣer texxamt. Tamentilt: %2$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s iqbel tinubga i %2$s. Tamentilt: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Tqebleḍ tinubga i %1$s. Tamentilt: %2$s</string>
<string name="notice_room_withdraw_with_reason">%1$s issefsex tinubga n %2$s. Tamentilt: %3$s</string>
<string name="notice_room_withdraw_with_reason_by_you">Tesfesxeḍ tinubga n %1$s. Tamentilt: %2$s</string>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s yerna %2$s d tansa i texxamt-a.</item>
<item quantity="other">%1$s yerna %2$s d tansiwin i texxamt-a.</item>
</plurals>
<plurals name="notice_room_aliases_added_by_you">
<item quantity="one">Terniḍ %1$s d tansa i texxamt-a.</item>
<item quantity="other">Terniḍ %1$s d tansiwin i texxamt-a.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s yekkes %2$s am tansa i texxamt-a.</item>
<item quantity="other">%1$s yekkes %3$s am tansiwin i texxamt-a.</item>
</plurals>
<plurals name="notice_room_aliases_removed_by_you">
<item quantity="one">Tekkseḍ %1$s am tansa i texxamt-a.</item>
<item quantity="other">Tekkseḍ %2$s am tansiwin i texxamt-a.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">%1$s yerna %2$s terniḍ tekkseḍ %3s am tansiwin i texxamt-a.</string>
<string name="notice_room_aliases_added_and_removed_by_you">Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a.</string>
<string name="notice_room_canonical_alias_set">%1$s isbadu %2$s am tansa tagejdant i texxamt-a.</string>
<string name="notice_room_canonical_alias_set_by_you">Tesbaduḍ %1$s am tansa tagejdant i texxamt-a.</string>
<string name="notice_room_canonical_alias_unset">%1$s yekkes tansa tagejdant i texxamt-a.</string>
<string name="notice_room_canonical_alias_unset_by_you">Tekkseḍ tansa tagejdant i texxamt-a.</string>
<string name="notice_room_guest_access_can_join">%1$s isireg inebgawen ad ddun ɣer texxamt.</string>
<string name="notice_room_guest_access_can_join_by_you">Tsirgeḍ inebgawen ad ddun ɣer texxamt.</string>
<string name="notice_room_guest_access_forbidden">%1$s issewḥel inebgawen iwakken ur tteddun ara ɣer texxamt.</string>
<string name="notice_room_guest_access_forbidden_by_you">Tesweḥleḍ inebgawen iwakken ur tteddun ara ɣer texxamt.</string>
<string name="notice_end_to_end_ok">%1$s yermed awgelhen seg yixef ɣer yixef.</string>
<string name="notice_end_to_end_ok_by_you">Tremdeḍ awgelhen seg yixef ɣer yixef.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s yermed awgelhen seg yixef ɣer yixef (alguritm %2$s ur yettwassen ara).</string>
<string name="clear_timeline_send_queue">Sfeḍ tabdart n uraǧu n tuzzna</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s issefsex tinubga n %2$s i tmerniwt ɣer texxamt. Tamentilt: %2$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Tesfesxeḍ tinubga n %1$s i tmerna ɣer texxamt. Tamentilt: %2$s</string>
<string name="could_not_redact">Yegguma ad yaru</string>
</resources>

View file

@ -2,7 +2,6 @@
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="notice_room_invite_no_invitee">%s님의 초대</string>
<string name="verification_emoji_headphone">헤드폰</string>
<string name="summary_user_sent_image">%1$s님이 사진을 보냈습니다.</string>
<string name="summary_user_sent_sticker">%1$s님이 스티커를 보냈습니다.</string>
@ -78,71 +77,6 @@
<string name="room_displayname_empty_room">빈 방</string>
<string name="verification_emoji_dog"></string>
<string name="verification_emoji_cat">고양이</string>
<string name="verification_emoji_lion">사자</string>
<string name="verification_emoji_horse"></string>
<string name="verification_emoji_unicorn">유니콘</string>
<string name="verification_emoji_pig">돼지</string>
<string name="verification_emoji_elephant">코끼리</string>
<string name="verification_emoji_rabbit">토끼</string>
<string name="verification_emoji_panda">판다</string>
<string name="verification_emoji_rooster">수탉</string>
<string name="verification_emoji_penguin">펭귄</string>
<string name="verification_emoji_turtle">거북</string>
<string name="verification_emoji_fish">물고기</string>
<string name="verification_emoji_octopus">문어</string>
<string name="verification_emoji_butterfly">나비</string>
<string name="verification_emoji_flower"></string>
<string name="verification_emoji_tree">나무</string>
<string name="verification_emoji_cactus">선인장</string>
<string name="verification_emoji_mushroom">버섯</string>
<string name="verification_emoji_globe">지구본</string>
<string name="verification_emoji_moon"></string>
<string name="verification_emoji_cloud">구름</string>
<string name="verification_emoji_fire"></string>
<string name="verification_emoji_banana">바나나</string>
<string name="verification_emoji_apple">사과</string>
<string name="verification_emoji_strawberry">딸기</string>
<string name="verification_emoji_corn">옥수수</string>
<string name="verification_emoji_pizza">피자</string>
<string name="verification_emoji_cake">케이크</string>
<string name="verification_emoji_heart">하트</string>
<string name="verification_emoji_smiley">웃음</string>
<string name="verification_emoji_robot">로봇</string>
<string name="verification_emoji_hat">모자</string>
<string name="verification_emoji_glasses">안경</string>
<string name="verification_emoji_wrench">스패너</string>
<string name="verification_emoji_santa">산타클로스</string>
<string name="verification_emoji_thumbsup">좋아요</string>
<string name="verification_emoji_umbrella">우산</string>
<string name="verification_emoji_hourglass">모래시계</string>
<string name="verification_emoji_clock">시계</string>
<string name="verification_emoji_gift">선물</string>
<string name="verification_emoji_lightbulb">전구</string>
<string name="verification_emoji_book"></string>
<string name="verification_emoji_pencil">연필</string>
<string name="verification_emoji_paperclip">클립</string>
<string name="verification_emoji_scissors">가위</string>
<string name="verification_emoji_lock">자물쇠</string>
<string name="verification_emoji_key">열쇠</string>
<string name="verification_emoji_hammer">망치</string>
<string name="verification_emoji_telephone">전화기</string>
<string name="verification_emoji_flag">깃발</string>
<string name="verification_emoji_train">기차</string>
<string name="verification_emoji_bicycle">자전거</string>
<string name="verification_emoji_airplane">비행기</string>
<string name="verification_emoji_rocket">로켓</string>
<string name="verification_emoji_trophy">트로피</string>
<string name="verification_emoji_ball"></string>
<string name="verification_emoji_guitar">기타</string>
<string name="verification_emoji_trumpet">트럼펫</string>
<string name="verification_emoji_bell"></string>
<string name="verification_emoji_anchor"></string>
<string name="verification_emoji_folder">폴더</string>
<string name="verification_emoji_pin"></string>
<string name="initial_sync_start_importing_account">초기 동기화:
\n계정 가져오는 중…</string>
<string name="initial_sync_start_importing_account_crypto">초기 동기화:

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Hund</string>
<string name="verification_emoji_cat">Katt</string>
<string name="verification_emoji_lion">Løve</string>
<string name="verification_emoji_horse">Hest</string>
<string name="verification_emoji_unicorn">Enhjørning</string>
<string name="verification_emoji_pig">Gris</string>
<string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Kanin</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hane</string>
<string name="verification_emoji_penguin">Pingvin</string>
<string name="verification_emoji_turtle">Skilpadde</string>
<string name="verification_emoji_fish">Fisk</string>
<string name="verification_emoji_octopus">Blekksprut</string>
<string name="verification_emoji_butterfly">Sommerfugl</string>
<string name="verification_emoji_flower">Blomst</string>
<string name="verification_emoji_tree">Tre</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Sopp</string>
<string name="verification_emoji_globe">Globus</string>
<string name="verification_emoji_moon">Måne</string>
<string name="verification_emoji_cloud">Sky</string>
<string name="verification_emoji_fire">Flamme</string>
<string name="verification_emoji_banana">Banan</string>
<string name="verification_emoji_apple">Eple</string>
<string name="verification_emoji_strawberry">Jordbær</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Kake</string>
<string name="verification_emoji_heart">Hjerte</string>
<string name="verification_emoji_smiley">Smilefjes</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Hatt</string>
<string name="verification_emoji_glasses">Briller</string>
<string name="verification_emoji_spanner">Fastnøkkel</string>
<string name="verification_emoji_santa">Julenisse</string>
<string name="verification_emoji_thumbs_up">Tommel Opp</string>
<string name="verification_emoji_umbrella">Paraply</string>
<string name="verification_emoji_hourglass">Timeglass</string>
<string name="verification_emoji_clock">Klokke</string>
<string name="verification_emoji_gift">Gave</string>
<string name="verification_emoji_light_bulb">Lyspære</string>
<string name="verification_emoji_book">Bok</string>
<string name="verification_emoji_pencil">Blyant</string>
<string name="verification_emoji_paperclip">BInders</string>
<string name="verification_emoji_scissors">Saks</string>
<string name="verification_emoji_lock">Lås</string>
<string name="verification_emoji_key">Nøkkel</string>
<string name="verification_emoji_hammer">Hammer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagg</string>
<string name="verification_emoji_train">Tog</string>
<string name="verification_emoji_bicycle">Sykkel</string>
<string name="verification_emoji_aeroplane">Fly</string>
<string name="verification_emoji_rocket">Rakett</string>
<string name="verification_emoji_trophy">Pokal</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitar</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Bjelle</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphones">Hodetelefoner</string>
<string name="verification_emoji_folder">Mappe</string>
<string name="verification_emoji_pin">Tegnestift</string>
</resources>

View file

@ -87,70 +87,6 @@
<string name="notice_event_redacted_by">Bericht verwijderd door %1$s</string>
<string name="notice_event_redacted_with_reason">Bericht verwijderd [reden: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Bericht verwijderd door %1$s [reden: %2$s]</string>
<string name="verification_emoji_dog">Hond</string>
<string name="verification_emoji_cat">Kat</string>
<string name="verification_emoji_lion">Leeuw</string>
<string name="verification_emoji_horse">Paard</string>
<string name="verification_emoji_unicorn">Eenhoorn</string>
<string name="verification_emoji_pig">Varken</string>
<string name="verification_emoji_elephant">Olifant</string>
<string name="verification_emoji_rabbit">Konijn</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Haan</string>
<string name="verification_emoji_penguin">Pinguïn</string>
<string name="verification_emoji_turtle">Schildpad</string>
<string name="verification_emoji_fish">Vis</string>
<string name="verification_emoji_octopus">Octopus</string>
<string name="verification_emoji_butterfly">Vlinder</string>
<string name="verification_emoji_flower">Bloem</string>
<string name="verification_emoji_tree">Boom</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Paddenstoel</string>
<string name="verification_emoji_globe">Aardbol</string>
<string name="verification_emoji_moon">Maan</string>
<string name="verification_emoji_cloud">Wolk</string>
<string name="verification_emoji_fire">Vuur</string>
<string name="verification_emoji_banana">Banaan</string>
<string name="verification_emoji_apple">Appel</string>
<string name="verification_emoji_strawberry">Aardbei</string>
<string name="verification_emoji_corn">Maïs</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Taart</string>
<string name="verification_emoji_heart">Hart</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Hoed</string>
<string name="verification_emoji_glasses">Bril</string>
<string name="verification_emoji_wrench">Moersleutel</string>
<string name="verification_emoji_santa">Kerstman</string>
<string name="verification_emoji_thumbsup">Duim omhoog</string>
<string name="verification_emoji_umbrella">Paraplu</string>
<string name="verification_emoji_hourglass">Zandloper</string>
<string name="verification_emoji_clock">Klok</string>
<string name="verification_emoji_gift">Cadeau</string>
<string name="verification_emoji_lightbulb">Gloeilamp</string>
<string name="verification_emoji_book">Boek</string>
<string name="verification_emoji_pencil">Potlood</string>
<string name="verification_emoji_paperclip">Paperclip</string>
<string name="verification_emoji_scissors">Schaar</string>
<string name="verification_emoji_lock">Slot</string>
<string name="verification_emoji_key">Sleutel</string>
<string name="verification_emoji_hammer">Hamer</string>
<string name="verification_emoji_telephone">Telefoon</string>
<string name="verification_emoji_flag">Vlag</string>
<string name="verification_emoji_train">Trein</string>
<string name="verification_emoji_bicycle">Fiets</string>
<string name="verification_emoji_airplane">Vliegtuig</string>
<string name="verification_emoji_rocket">Raket</string>
<string name="verification_emoji_trophy">Trofee</string>
<string name="verification_emoji_ball">Bal</string>
<string name="verification_emoji_guitar">Gitaar</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Bel</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphone">Koptelefoon</string>
<string name="verification_emoji_folder">Map</string>
<string name="verification_emoji_pin">Speld</string>
<string name="initial_sync_start_importing_account">Initiële synchronisatie:
\nAccount wordt geïmporteerd…</string>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Hond</string>
<string name="verification_emoji_cat">Kat</string>
<string name="verification_emoji_lion">Leeuw</string>
<string name="verification_emoji_horse">Paard</string>
<string name="verification_emoji_unicorn">Eenhoorn</string>
<string name="verification_emoji_pig">Varken</string>
<string name="verification_emoji_elephant">Olifant</string>
<string name="verification_emoji_rabbit">Konijn</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Haan</string>
<string name="verification_emoji_penguin">Pinguïn</string>
<string name="verification_emoji_turtle">Schildpad</string>
<string name="verification_emoji_fish">Vis</string>
<string name="verification_emoji_octopus">Octopus</string>
<string name="verification_emoji_butterfly">Vlinder</string>
<string name="verification_emoji_flower">Bloem</string>
<string name="verification_emoji_tree">Boom</string>
<string name="verification_emoji_cactus">Cactus</string>
<string name="verification_emoji_mushroom">Paddenstoel</string>
<string name="verification_emoji_globe">Wereldbol</string>
<string name="verification_emoji_moon">Maan</string>
<string name="verification_emoji_cloud">Wolk</string>
<string name="verification_emoji_fire">Vuur</string>
<string name="verification_emoji_banana">Banaan</string>
<string name="verification_emoji_apple">Appel</string>
<string name="verification_emoji_strawberry">Aardbei</string>
<string name="verification_emoji_corn">Maïs</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Taart</string>
<string name="verification_emoji_heart">Hart</string>
<string name="verification_emoji_smiley">Smiley</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Hoed</string>
<string name="verification_emoji_glasses">Bril</string>
<string name="verification_emoji_spanner">Moersleutel</string>
<string name="verification_emoji_santa">Kerstman</string>
<string name="verification_emoji_thumbs_up">Duim omhoog</string>
<string name="verification_emoji_umbrella">Paraplu</string>
<string name="verification_emoji_hourglass">Zandloper</string>
<string name="verification_emoji_clock">Wekker</string>
<string name="verification_emoji_gift">Geschenk</string>
<string name="verification_emoji_light_bulb">Gloeilamp</string>
<string name="verification_emoji_book">Boek</string>
<string name="verification_emoji_pencil">Potlood</string>
<string name="verification_emoji_paperclip">Papierklemmetje</string>
<string name="verification_emoji_scissors">Schaar</string>
<string name="verification_emoji_lock">Slot</string>
<string name="verification_emoji_key">Sleutel</string>
<string name="verification_emoji_hammer">Hamer</string>
<string name="verification_emoji_telephone">Telefoon</string>
<string name="verification_emoji_flag">Vlag</string>
<string name="verification_emoji_train">Trein</string>
<string name="verification_emoji_bicycle">Fiets</string>
<string name="verification_emoji_aeroplane">Vliegtuig</string>
<string name="verification_emoji_rocket">Raket</string>
<string name="verification_emoji_trophy">Trofee</string>
<string name="verification_emoji_ball">Bal</string>
<string name="verification_emoji_guitar">Gitaar</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Bel</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphones">Koptelefoon</string>
<string name="verification_emoji_folder">Map</string>
<string name="verification_emoji_pin">Duimspijker</string>
</resources>

View file

@ -77,70 +77,6 @@
<string name="notice_event_redacted_by">%1$s strauk meldingi</string>
<string name="notice_event_redacted_with_reason">Meldingi vart stroki [av di: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">%1$s strauk meldingi [av di: %2$s]</string>
<string name="verification_emoji_dog">Hund</string>
<string name="verification_emoji_cat">Katt</string>
<string name="verification_emoji_lion">Løva</string>
<string name="verification_emoji_horse">Hest</string>
<string name="verification_emoji_unicorn">Einhyrning</string>
<string name="verification_emoji_pig">Gris</string>
<string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Hare</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hane</string>
<string name="verification_emoji_penguin">Pingvin</string>
<string name="verification_emoji_turtle">Skjoldpadda</string>
<string name="verification_emoji_fish">Fisk</string>
<string name="verification_emoji_octopus">Blekksprut</string>
<string name="verification_emoji_butterfly">Fivrelde</string>
<string name="verification_emoji_flower">Blome</string>
<string name="verification_emoji_tree">Tre</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Sopp</string>
<string name="verification_emoji_globe">Klote</string>
<string name="verification_emoji_moon">Måne</string>
<string name="verification_emoji_cloud">Sky</string>
<string name="verification_emoji_fire">Eld</string>
<string name="verification_emoji_banana">Banan</string>
<string name="verification_emoji_apple">Eple</string>
<string name="verification_emoji_strawberry">Jordbær</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Kaka</string>
<string name="verification_emoji_heart">Hjarta</string>
<string name="verification_emoji_smiley">Smilandlit</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Hatt</string>
<string name="verification_emoji_glasses">Brillor</string>
<string name="verification_emoji_wrench">Skiftenykel</string>
<string name="verification_emoji_santa">Nissen</string>
<string name="verification_emoji_thumbsup">Tumalen Upp</string>
<string name="verification_emoji_umbrella">Regnskjold</string>
<string name="verification_emoji_hourglass">Timeglas</string>
<string name="verification_emoji_clock">Ur</string>
<string name="verification_emoji_gift">Gåva</string>
<string name="verification_emoji_lightbulb">Ljospera</string>
<string name="verification_emoji_book">Bok</string>
<string name="verification_emoji_pencil">Blyant</string>
<string name="verification_emoji_paperclip">Binders</string>
<string name="verification_emoji_scissors">Saks</string>
<string name="verification_emoji_lock">Lås</string>
<string name="verification_emoji_key">Nykel</string>
<string name="verification_emoji_hammer">Hamar</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagg</string>
<string name="verification_emoji_train">Tog</string>
<string name="verification_emoji_bicycle">Sykkel</string>
<string name="verification_emoji_airplane">Flyg</string>
<string name="verification_emoji_rocket">Rakett</string>
<string name="verification_emoji_trophy">Pokal</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitar</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Klokka</string>
<string name="verification_emoji_anchor">Ankar</string>
<string name="verification_emoji_headphone">Hodetelefon</string>
<string name="verification_emoji_folder">Mappa</string>
<string name="verification_emoji_pin">Nål</string>
<string name="notice_room_update">%s oppgraderte rommet.</string>

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