Unverified Commit 37ec7429 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge branch 'develop' into new/analytics-for-password-reset

parents 465239c8 644d6518
...@@ -4,11 +4,14 @@ ...@@ -4,11 +4,14 @@
[![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Build Status](https://travis-ci.org/RocketChat/Rocket.Chat.Android.svg?branch=develop)](https://travis-ci.org/RocketChat/Rocket.Chat.Android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android) [![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Build Status](https://travis-ci.org/RocketChat/Rocket.Chat.Android.svg?branch=develop)](https://travis-ci.org/RocketChat/Rocket.Chat.Android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android)
## Get it from the stores
[![](https://user-images.githubusercontent.com/551004/48210434-74c07100-e35e-11e8-8eee-3ba84ffa74d7.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android/)
## Description ## Description
This repository contains all the code related to the Android native application of [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/#about-rocketchat). To send new pull-requests, always use the branch `develop` as base and open an issue with the description of what you want/need to accomplish, if the issue wasn't created yet. This repository contains all the code related to the Android native application of [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/#about-rocketchat). To send new pull-requests, always use the branch `develop` as base and open an issue with the description of what you want/need to accomplish, if the issue wasn't created yet.
## How to build ## How to build
- You need to download the latest [Android Studio Preview](https://developer.android.com/studio/preview/) version since the stable IDE version does not support the [JetPack](https://developer.android.com/jetpack/) that is being used on this application. - You need to download the latest [Android Studio Preview](https://developer.android.com/studio/preview/) version since the stable IDE version does not support the [JetPack](https://developer.android.com/jetpack/) that is being used on this application.
...@@ -39,7 +42,7 @@ cd Rocket.Chat.Android/app ...@@ -39,7 +42,7 @@ cd Rocket.Chat.Android/app
## Bug report & Feature request ## Bug report & Feature request
Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Android/issues) :) Are you having a technical issue trying to compile the app, or setting up Push Notifications? Please use our Community Support channel for that: https://forums.rocket.chat/c/community-support. The issues are only suppose to be used for bugs, improvements and features in the native Android application.
## Coding Style ## Coding Style
......
...@@ -16,8 +16,8 @@ android { ...@@ -16,8 +16,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2048 versionCode 2050
versionName "3.0.0" versionName "3.1.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
......
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "db46c12dbb8747200288f48d5dc5558b",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `username` TEXT, `name` TEXT, `status` TEXT NOT NULL, `utcOffset` REAL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "utcOffset",
"columnName": "utcOffset",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_users_username",
"unique": false,
"columnNames": [
"username"
],
"createSql": "CREATE INDEX `index_users_username` ON `${TABLE_NAME}` (`username`)"
}
],
"foreignKeys": []
},
{
"tableName": "chatrooms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `fullname` TEXT, `userId` TEXT, `ownerId` TEXT, `readonly` INTEGER, `isDefault` INTEGER, `favorite` INTEGER, `open` INTEGER NOT NULL, `alert` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `userMentions` INTEGER, `groupMentions` INTEGER, `updatedAt` INTEGER, `timestamp` INTEGER, `lastSeen` INTEGER, `lastMessageText` TEXT, `lastMessageUserId` TEXT, `lastMessageTimestamp` INTEGER, `broadcast` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`ownerId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`lastMessageUserId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscriptionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fullname",
"columnName": "fullname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "readonly",
"columnName": "readonly",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "favorite",
"columnName": "favorite",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "open",
"columnName": "open",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alert",
"columnName": "alert",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userMentions",
"columnName": "userMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "groupMentions",
"columnName": "groupMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessageText",
"columnName": "lastMessageText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageUserId",
"columnName": "lastMessageUserId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageTimestamp",
"columnName": "lastMessageTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "broadcast",
"columnName": "broadcast",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_chatrooms_userId",
"unique": false,
"columnNames": [
"userId"
],
"createSql": "CREATE INDEX `index_chatrooms_userId` ON `${TABLE_NAME}` (`userId`)"
},
{
"name": "index_chatrooms_ownerId",
"unique": false,
"columnNames": [
"ownerId"
],
"createSql": "CREATE INDEX `index_chatrooms_ownerId` ON `${TABLE_NAME}` (`ownerId`)"
},
{
"name": "index_chatrooms_subscriptionId",
"unique": true,
"columnNames": [
"subscriptionId"
],
"createSql": "CREATE UNIQUE INDEX `index_chatrooms_subscriptionId` ON `${TABLE_NAME}` (`subscriptionId`)"
},
{
"name": "index_chatrooms_updatedAt",
"unique": false,
"columnNames": [
"updatedAt"
],
"createSql": "CREATE INDEX `index_chatrooms_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
},
{
"name": "index_chatrooms_lastMessageUserId",
"unique": false,
"columnNames": [
"lastMessageUserId"
],
"createSql": "CREATE INDEX `index_chatrooms_lastMessageUserId` ON `${TABLE_NAME}` (`lastMessageUserId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"ownerId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"lastMessageUserId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `roomId` TEXT NOT NULL, `message` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `senderId` TEXT, `updatedAt` INTEGER, `editedAt` INTEGER, `editedBy` TEXT, `senderAlias` TEXT, `avatar` TEXT, `type` TEXT, `groupable` INTEGER NOT NULL, `parseUrls` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `role` TEXT, `synced` INTEGER NOT NULL, `unread` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`senderId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`editedBy`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "roomId",
"columnName": "roomId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "senderId",
"columnName": "senderId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "editedAt",
"columnName": "editedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "editedBy",
"columnName": "editedBy",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "senderAlias",
"columnName": "senderAlias",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "groupable",
"columnName": "groupable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "parseUrls",
"columnName": "parseUrls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "role",
"columnName": "role",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "synced",
"columnName": "synced",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"senderId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"editedBy"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message_favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `userId` TEXT NOT NULL, PRIMARY KEY(`messageId`, `userId`), FOREIGN KEY(`messageId`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"messageId",
"userId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"messageId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message_mentions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `userId` TEXT NOT NULL, PRIMARY KEY(`messageId`, `userId`), FOREIGN KEY(`messageId`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"messageId",
"userId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"messageId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message_channels",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `roomId` TEXT NOT NULL, `roomName` TEXT, PRIMARY KEY(`messageId`, `roomId`), FOREIGN KEY(`messageId`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "roomId",
"columnName": "roomId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "roomName",
"columnName": "roomName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"messageId",
"roomId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"messageId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachments",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `message_id` TEXT NOT NULL, `title` TEXT, `type` TEXT, `description` TEXT, `text` TEXT, `author_name` TEXT, `author_icon` TEXT, `author_link` TEXT, `thumb_url` TEXT, `color` TEXT, `fallback` TEXT, `title_link` TEXT, `title_link_download` INTEGER NOT NULL, `image_url` TEXT, `image_type` TEXT, `image_size` INTEGER, `video_url` TEXT, `video_type` TEXT, `video_size` INTEGER, `audio_url` TEXT, `audio_type` TEXT, `audio_size` INTEGER, `message_link` TEXT, `timestamp` INTEGER, `has_actions` INTEGER NOT NULL, `has_fields` INTEGER NOT NULL, `button_alignment` TEXT, PRIMARY KEY(`_id`), FOREIGN KEY(`message_id`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "_id",
"columnName": "_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "messageId",
"columnName": "message_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorName",
"columnName": "author_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorIcon",
"columnName": "author_icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorLink",
"columnName": "author_link",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbUrl",
"columnName": "thumb_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fallback",
"columnName": "fallback",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleLink",
"columnName": "title_link",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleLinkDownload",
"columnName": "title_link_download",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageUrl",
"columnName": "image_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageType",
"columnName": "image_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageSize",
"columnName": "image_size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "videoUrl",
"columnName": "video_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoType",
"columnName": "video_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoSize",
"columnName": "video_size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "audioUrl",
"columnName": "audio_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioType",
"columnName": "audio_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "audioSize",
"columnName": "audio_size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "messageLink",
"columnName": "message_link",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "hasActions",
"columnName": "has_actions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasFields",
"columnName": "has_fields",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "buttonAlignment",
"columnName": "button_alignment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment_fields",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `attachmentId` TEXT NOT NULL, `title` TEXT NOT NULL, `value` TEXT NOT NULL, FOREIGN KEY(`attachmentId`) REFERENCES `attachments`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachmentId",
"columnName": "attachmentId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_fields_attachmentId",
"unique": false,
"columnNames": [
"attachmentId"
],
"createSql": "CREATE INDEX `index_attachment_fields_attachmentId` ON `${TABLE_NAME}` (`attachmentId`)"
}
],
"foreignKeys": [
{
"table": "attachments",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"attachmentId"
],
"referencedColumns": [
"_id"
]
}
]
},
{
"tableName": "attachment_action",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `attachmentId` TEXT NOT NULL, `type` TEXT NOT NULL, `text` TEXT, `url` TEXT, `isWebView` INTEGER, `webViewHeightRatio` TEXT, `imageUrl` TEXT, `message` TEXT, `isMessageInChatWindow` INTEGER, FOREIGN KEY(`attachmentId`) REFERENCES `attachments`(`_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachmentId",
"columnName": "attachmentId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isWebView",
"columnName": "isWebView",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "webViewHeightRatio",
"columnName": "webViewHeightRatio",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageUrl",
"columnName": "imageUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isMessageInChatWindow",
"columnName": "isMessageInChatWindow",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_action_attachmentId",
"unique": false,
"columnNames": [
"attachmentId"
],
"createSql": "CREATE INDEX `index_attachment_action_attachmentId` ON `${TABLE_NAME}` (`attachmentId`)"
}
],
"foreignKeys": [
{
"table": "attachments",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"attachmentId"
],
"referencedColumns": [
"_id"
]
}
]
},
{
"tableName": "urls",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlId` INTEGER PRIMARY KEY AUTOINCREMENT, `messageId` TEXT NOT NULL, `url` TEXT NOT NULL, `hostname` TEXT, `title` TEXT, `description` TEXT, `imageUrl` TEXT, FOREIGN KEY(`messageId`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "urlId",
"columnName": "urlId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hostname",
"columnName": "hostname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageUrl",
"columnName": "imageUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"urlId"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_urls_messageId",
"unique": false,
"columnNames": [
"messageId"
],
"createSql": "CREATE INDEX `index_urls_messageId` ON `${TABLE_NAME}` (`messageId`)"
}
],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"messageId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "reactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `messageId` TEXT NOT NULL, `count` INTEGER NOT NULL, `usernames` TEXT NOT NULL, PRIMARY KEY(`reaction`), FOREIGN KEY(`messageId`) REFERENCES `messages`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "reaction",
"columnName": "reaction",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "count",
"columnName": "count",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "usernames",
"columnName": "usernames",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"reaction"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_reactions_messageId",
"unique": false,
"columnNames": [
"messageId"
],
"createSql": "CREATE INDEX `index_reactions_messageId` ON `${TABLE_NAME}` (`messageId`)"
}
],
"foreignKeys": [
{
"table": "messages",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"messageId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "messages_sync",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`roomId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`roomId`))",
"fields": [
{
"fieldPath": "roomId",
"columnName": "roomId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"roomId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"db46c12dbb8747200288f48d5dc5558b\")"
]
}
}
\ No newline at end of file
...@@ -113,25 +113,26 @@ interface LoginOptionsView : LoadingView, MessageView { ...@@ -113,25 +113,26 @@ interface LoginOptionsView : LoadingView, MessageView {
// CAS account. // CAS account.
/** /**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server * Adds a CAS button into accounts container.
* settings.
* *
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener]. * @param casUrl The CAS url.
* @param casToken The CAS token
* @param serviceName The SAML service name.
* @param serviceNameColor The SAML service name color (just stylizing).
* @param buttonColor The SAML button color (just stylizing).
* @see [showAccountsView] * @see [showAccountsView]
*/ */
fun enableLoginByCas() fun addCasButton(
caslUrl: String,
/** casToken: String,
* Setups the CAS button. serviceName: String,
* serviceNameColor: Int,
* @param casUrl The CAS URL to authenticate with. buttonColor: Int
* @param casToken The requested token to be sent to the CAS server. )
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
// Custom OAuth account. // Custom OAuth account.
/** /**
* Adds a custom OAuth button in the accounts container. * Adds a custom OAuth button into accounts container.
* *
* @customOauthUrl The custom OAuth url. * @customOauthUrl The custom OAuth url.
* @state A random string generated by the app, which you'll verify later * @state A random string generated by the app, which you'll verify later
...@@ -151,12 +152,13 @@ interface LoginOptionsView : LoadingView, MessageView { ...@@ -151,12 +152,13 @@ interface LoginOptionsView : LoadingView, MessageView {
// SAML account. // SAML account.
/** /**
* Adds a SAML button in the accounts container. * Adds a SAML button into accounts container.
* *
* @samlUrl The SAML url. * @param samlUrl The SAML url.
* @serviceName The SAML service name. * @param samlToken The SAML token.
* @serviceNameColor The SAML service name color (just stylizing). * @param serviceName The SAML service name.
* @buttonColor The SAML button color (just stylizing). * @param serviceNameColor The SAML service name color (just stylizing).
* @param buttonColor The SAML button color (just stylizing).
* @see [showAccountsView] * @see [showAccountsView]
*/ */
fun addSamlButton( fun addSamlButton(
......
...@@ -40,6 +40,9 @@ private const val GITLAB_OAUTH_URL = "gitlab_oauth_url" ...@@ -40,6 +40,9 @@ private const val GITLAB_OAUTH_URL = "gitlab_oauth_url"
private const val WORDPRESS_OAUTH_URL = "wordpress_oauth_url" private const val WORDPRESS_OAUTH_URL = "wordpress_oauth_url"
private const val CAS_LOGIN_URL = "cas_login_url" private const val CAS_LOGIN_URL = "cas_login_url"
private const val CAS_TOKEN = "cas_token" private const val CAS_TOKEN = "cas_token"
private const val CAS_SERVICE_NAME = "cas_service_name"
private const val CAS_SERVICE_NAME_TEXT_COLOR = "cas_service_name_text_color"
private const val CAS_SERVICE_BUTTON_COLOR = "cas_service_button_color"
private const val CUSTOM_OAUTH_URL = "custom_oauth_url" private const val CUSTOM_OAUTH_URL = "custom_oauth_url"
private const val CUSTOM_OAUTH_SERVICE_NAME = "custom_oauth_service_name" private const val CUSTOM_OAUTH_SERVICE_NAME = "custom_oauth_service_name"
private const val CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR = "custom_oauth_service_name_text_color" private const val CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR = "custom_oauth_service_name_text_color"
...@@ -69,6 +72,9 @@ fun newInstance( ...@@ -69,6 +72,9 @@ fun newInstance(
wordpressOauthUrl: String? = null, wordpressOauthUrl: String? = null,
casLoginUrl: String? = null, casLoginUrl: String? = null,
casToken: String? = null, casToken: String? = null,
casServiceName: String? = null,
casServiceNameTextColor: Int = 0,
casServiceButtonColor: Int = 0,
customOauthUrl: String? = null, customOauthUrl: String? = null,
customOauthServiceName: String? = null, customOauthServiceName: String? = null,
customOauthServiceNameTextColor: Int = 0, customOauthServiceNameTextColor: Int = 0,
...@@ -95,6 +101,9 @@ fun newInstance( ...@@ -95,6 +101,9 @@ fun newInstance(
putString(WORDPRESS_OAUTH_URL, wordpressOauthUrl) putString(WORDPRESS_OAUTH_URL, wordpressOauthUrl)
putString(CAS_LOGIN_URL, casLoginUrl) putString(CAS_LOGIN_URL, casLoginUrl)
putString(CAS_TOKEN, casToken) putString(CAS_TOKEN, casToken)
putString(CAS_SERVICE_NAME, casServiceName)
putInt(CAS_SERVICE_NAME_TEXT_COLOR, casServiceNameTextColor)
putInt(CAS_SERVICE_BUTTON_COLOR, casServiceButtonColor)
putString(CUSTOM_OAUTH_URL, customOauthUrl) putString(CUSTOM_OAUTH_URL, customOauthUrl)
putString(CUSTOM_OAUTH_SERVICE_NAME, customOauthServiceName) putString(CUSTOM_OAUTH_SERVICE_NAME, customOauthServiceName)
putInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR, customOauthServiceNameTextColor) putInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR, customOauthServiceNameTextColor)
...@@ -127,6 +136,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -127,6 +136,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
private var wordpressOauthUrl: String? = null private var wordpressOauthUrl: String? = null
private var casLoginUrl: String? = null private var casLoginUrl: String? = null
private var casToken: String? = null private var casToken: String? = null
private var casServiceName: String? = null
private var casServiceNameTextColor: Int = 0
private var casServiceButtonColor: Int = 0
private var customOauthUrl: String? = null private var customOauthUrl: String? = null
private var customOauthServiceName: String? = null private var customOauthServiceName: String? = null
private var customOauthServiceTextColor: Int = 0 private var customOauthServiceTextColor: Int = 0
...@@ -157,6 +169,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -157,6 +169,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
wordpressOauthUrl = bundle.getString(WORDPRESS_OAUTH_URL) wordpressOauthUrl = bundle.getString(WORDPRESS_OAUTH_URL)
casLoginUrl = bundle.getString(CAS_LOGIN_URL) casLoginUrl = bundle.getString(CAS_LOGIN_URL)
casToken = bundle.getString(CAS_TOKEN) casToken = bundle.getString(CAS_TOKEN)
casServiceName = bundle.getString(CAS_SERVICE_NAME)
casServiceNameTextColor = bundle.getInt(CAS_SERVICE_NAME_TEXT_COLOR)
casServiceButtonColor = bundle.getInt(CAS_SERVICE_BUTTON_COLOR)
customOauthUrl = bundle.getString(CUSTOM_OAUTH_URL) customOauthUrl = bundle.getString(CUSTOM_OAUTH_URL)
customOauthServiceName = bundle.getString(CUSTOM_OAUTH_SERVICE_NAME) customOauthServiceName = bundle.getString(CUSTOM_OAUTH_SERVICE_NAME)
customOauthServiceTextColor = bundle.getInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR) customOauthServiceTextColor = bundle.getInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR)
...@@ -200,6 +215,7 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -200,6 +215,7 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupCas() setupCas()
setupCustomOauth() setupCustomOauth()
setupSaml() setupSaml()
setupAccountsView()
setupLoginWithEmailView() setupLoginWithEmailView()
setupCreateNewAccountView() setupCreateNewAccountView()
} }
...@@ -235,19 +251,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -235,19 +251,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupWordpressButtonListener(wordpressOauthUrl.toString(), state.toString()) setupWordpressButtonListener(wordpressOauthUrl.toString(), state.toString())
enableLoginByWordpress() enableLoginByWordpress()
} }
if (totalSocialAccountsEnabled > 0) {
showAccountsView()
if (totalSocialAccountsEnabled > 3) {
setupExpandAccountsView()
}
}
} }
private fun setupCas() { private fun setupCas() {
if (casLoginUrl != null && casToken != null) { if (casLoginUrl != null && casToken != null && casServiceName != null) {
setupCasButtonListener(casLoginUrl.toString(), casToken.toString()) addCasButton(
enableLoginByCas() casLoginUrl.toString(),
casToken.toString(),
casServiceName.toString(),
casServiceNameTextColor,
casServiceButtonColor
)
} }
} }
...@@ -275,6 +289,15 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -275,6 +289,15 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
} }
} }
private fun setupAccountsView() {
if (totalSocialAccountsEnabled > 0) {
showAccountsView()
if (totalSocialAccountsEnabled > 3) {
setupExpandAccountsView()
}
}
}
private fun setupLoginWithEmailView() { private fun setupLoginWithEmailView() {
if (isLoginFormEnabled) { if (isLoginFormEnabled) {
showLoginWithEmailButton() showLoginWithEmailButton()
...@@ -319,10 +342,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView { ...@@ -319,10 +342,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupButtonListener(button_wordpress, wordpressUrl, state, REQUEST_CODE_FOR_OAUTH) setupButtonListener(button_wordpress, wordpressUrl, state, REQUEST_CODE_FOR_OAUTH)
// CAS service account. // CAS service account.
override fun enableLoginByCas() = enableAccountButton(button_cas) override fun addCasButton(
caslUrl: String,
override fun setupCasButtonListener(casUrl: String, casToken: String) = casToken: String,
setupButtonListener(button_cas, casUrl, casToken, REQUEST_CODE_FOR_CAS) serviceName: String,
serviceNameColor: Int,
buttonColor: Int
) {
val button = getCustomServiceButton(serviceName, serviceNameColor, buttonColor)
setupButtonListener(button, caslUrl, casToken, REQUEST_CODE_FOR_CAS)
accounts_container.addView(button)
}
// Custom OAuth account. // Custom OAuth account.
override fun addCustomOauthButton( override fun addCustomOauthButton(
......
...@@ -23,10 +23,17 @@ class OnBoardingPresenter @Inject constructor( ...@@ -23,10 +23,17 @@ class OnBoardingPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor, val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor) { ) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun toSignInToYourServer() = navigator.toSignInToYourServer() fun toSignInToYourServer() = navigator.toSignInToYourServer()
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
fun connectToCommunityServer(communityServerUrl: String) { fun connectToCommunityServer(communityServerUrl: String) {
connectToServer(communityServerUrl) { connectToServer(communityServerUrl) {
if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) { if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) {
...@@ -43,6 +50,9 @@ class OnBoardingPresenter @Inject constructor( ...@@ -43,6 +50,9 @@ class OnBoardingPresenter @Inject constructor(
wordpressOauthUrl, wordpressOauthUrl,
casLoginUrl, casLoginUrl,
casToken, casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl, customOauthUrl,
customOauthServiceName, customOauthServiceName,
customOauthServiceNameTextColor, customOauthServiceNameTextColor,
...@@ -60,8 +70,6 @@ class OnBoardingPresenter @Inject constructor( ...@@ -60,8 +70,6 @@ class OnBoardingPresenter @Inject constructor(
} }
} }
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
private fun connectToServer(serverUrl: String, block: () -> Unit) { private fun connectToServer(serverUrl: String, block: () -> Unit) {
launchUI(strategy) { launchUI(strategy) {
// Check if we already have an account for this server... // Check if we already have an account for this server...
...@@ -73,11 +81,10 @@ class OnBoardingPresenter @Inject constructor( ...@@ -73,11 +81,10 @@ class OnBoardingPresenter @Inject constructor(
view.showLoading() view.showLoading()
try { try {
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
setupConnectionInfo(serverUrl) setupConnectionInfo(serverUrl)
// preparing next fragment before showing it // preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl) checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled() checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled() checkIfCreateNewAccountIsEnabled()
......
...@@ -30,6 +30,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -30,6 +30,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
wordpressOauthUrl: String? = null, wordpressOauthUrl: String? = null,
casLoginUrl: String? = null, casLoginUrl: String? = null,
casToken: String? = null, casToken: String? = null,
casServiceName: String? = null,
casServiceNameTextColor: Int = 0,
casServiceButtonColor: Int = 0,
customOauthUrl: String? = null, customOauthUrl: String? = null,
customOauthServiceName: String? = null, customOauthServiceName: String? = null,
customOauthServiceNameTextColor: Int = 0, customOauthServiceNameTextColor: Int = 0,
...@@ -59,6 +62,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -59,6 +62,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
wordpressOauthUrl, wordpressOauthUrl,
casLoginUrl, casLoginUrl,
casToken, casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl, customOauthUrl,
customOauthServiceName, customOauthServiceName,
customOauthServiceNameTextColor, customOauthServiceNameTextColor,
......
...@@ -25,7 +25,13 @@ class ServerPresenter @Inject constructor( ...@@ -25,7 +25,13 @@ class ServerPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor, val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor, view) { ) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
versionCheckView = view,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun checkServer(server: String) { fun checkServer(server: String) {
if (!server.isValidUrl()) { if (!server.isValidUrl()) {
...@@ -53,6 +59,9 @@ class ServerPresenter @Inject constructor( ...@@ -53,6 +59,9 @@ class ServerPresenter @Inject constructor(
wordpressOauthUrl, wordpressOauthUrl,
casLoginUrl, casLoginUrl,
casToken, casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl, customOauthUrl,
customOauthServiceName, customOauthServiceName,
customOauthServiceNameTextColor, customOauthServiceNameTextColor,
...@@ -90,11 +99,8 @@ class ServerPresenter @Inject constructor( ...@@ -90,11 +99,8 @@ class ServerPresenter @Inject constructor(
view.showLoading() view.showLoading()
try { try {
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it // preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl) checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled() checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled() checkIfCreateNewAccountIsEnabled()
...@@ -111,5 +117,4 @@ class ServerPresenter @Inject constructor( ...@@ -111,5 +117,4 @@ class ServerPresenter @Inject constructor(
} }
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ActionsAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import kotlinx.android.synthetic.main.item_actions_attachment.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import timber.log.Timber
class ActionsAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<ActionsAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(actions_attachment_container)
}
}
override fun bindViews(data: ActionsAttachmentUiModel) {
val actions = data.actions
val alignment = data.buttonAlignment
Timber.d("no of actions : ${actions.size} : $actions")
with(itemView) {
title.text = data.title ?: ""
actions_list.layoutManager = LinearLayoutManager(itemView.context,
when (alignment) {
"horizontal" -> LinearLayoutManager.HORIZONTAL
else -> LinearLayoutManager.VERTICAL //Default
}, false)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
}
interface ActionAttachmentOnClickListener {
fun onActionClicked(view: View, action: Action)
}
\ No newline at end of file
...@@ -70,4 +70,4 @@ class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListe ...@@ -70,4 +70,4 @@ class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListe
val action = actions[position] val action = actions[position]
holder.bindAction(action) holder.bindAction(action)
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.text.method.LinkMovementMethod import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageAttachmentUiModel import chat.rocket.android.chatroom.uimodel.AttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.isVisible
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.android.util.extensions.setOnClickListener
import chat.rocket.core.model.attachment.actions.Action
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.item_message_attachment.view.* import kotlinx.android.synthetic.main.item_message_attachment.view.*
import timber.log.Timber
class MessageAttachmentViewHolder( class AttachmentViewHolder(
itemView: View, itemView: View,
listener: ActionsListener, listener: ActionsListener,
reactionListener: EmojiReactionListener? = null reactionListener: EmojiReactionListener? = null,
) : BaseViewHolder<MessageAttachmentUiModel>(itemView, listener, reactionListener) { var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<AttachmentUiModel>(itemView, listener, reactionListener) {
private val messageViews = listOf<View>(
itemView.text_sender,
itemView.text_message_time,
itemView.text_content,
itemView.text_view_more
)
private val audioVideoViews = listOf<View>(
itemView.audio_video_attachment,
itemView.play_button
)
private val quoteBarColor = ContextCompat.getColor(itemView.context, R.color.quoteBar)
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container) setupActionMenu(attachment_container)
text_content.movementMethod = LinkMovementMethod() }
}
override fun bindViews(data: AttachmentUiModel) {
with(itemView) {
file_name.isVisible = false
text_file_name.isVisible = false
// Media attachments
image_attachment.isVisible = data.hasImage
audio_video_attachment.isVisible = data.hasAudioOrVideo
when {
data.hasImage -> bindImage(data)
data.hasAudioOrVideo -> bindAudioOrVideo(data)
data.hasFile -> bindFile(data)
}
// File description - self describing
file_description.isVisible = data.hasDescription
file_description.text = data.description
// Message attachment
messageViews.isVisible = data.hasMessage
if (data.hasMessage) {
bindMessage(data)
}
// Author
author_icon.isInvisible = !(data.hasAuthorIcon && data.hasAuthorLink && data.hasAuthorName)
text_author_name.isVisible = data.hasAuthorLink && data.hasAuthorName
if (data.hasAuthorLink && data.hasAuthorName) {
bindAuthorLink(data)
}
// If not media or message, show the text with quote bar
attachment_text.isVisible = !data.hasMedia && !data.hasMessage && data.hasText
attachment_text.text = data.text
// If it has titleLink and is not "type = file" show the title/titleLink on this field.
file_name_not_file_type.isVisible = !data.hasFile && data.hasTitleLink
if (!data.hasFile && data.hasTitleLink) {
bindTitleLink(data)
}
// Fields
text_fields.isVisible = data.hasFields
if (data.hasFields) {
bindFields(data)
}
// Actions
actions_list.isVisible = data.hasActions
if (data.hasActions) {
bindActions(data)
}
// Quote bar
quote_bar.isVisible = shouldShowQuoteBar(data)
if (data.color != null) {
quote_bar.setColorFilter(data.color)
} else {
quote_bar.setColorFilter(quoteBarColor)
}
}
}
private fun shouldShowQuoteBar(data: AttachmentUiModel): Boolean {
return data.hasFields || data.hasActions || (data.hasAuthorLink && data.hasAuthorName)
|| data.hasMessage || (!data.hasFile && data.hasTitleLink)
|| (!data.hasMedia && !data.hasMessage && data.hasText)
}
private fun bindImage(data: AttachmentUiModel) {
with(itemView) {
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(data.imageUrl)
autoPlayAnimations = true
oldController = image_attachment.controller
}.build()
image_attachment.controller = controller
image_attachment.setOnClickListener {
ImageHelper.openImage(
context,
data.imageUrl!!,
data.title?.toString()
)
}
file_name.isVisible = data.hasTitle
file_name.text = data.title
file_text.isVisible = data.hasText
file_text.text = data.text
}
}
private fun bindAudioOrVideo(data: AttachmentUiModel) {
with(itemView) {
text_file_name.content = data.title
file_text.isVisible = data.hasText
file_text.text = data.text
val url = if (data.hasVideo) data.videoUrl else data.audioUrl
audioVideoViews.setOnClickListener { view ->
url?.let {
PlayerActivity.play(view.context, url)
}
}
}
}
private fun bindFile(data: AttachmentUiModel) {
with(itemView) {
text_file_name.isVisible = true
text_file_name.content = data.title
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, data.titleLink?.toUri()))
}
} }
} }
override fun bindViews(data: MessageAttachmentUiModel) { private fun bindMessage(data: AttachmentUiModel) {
with(itemView) { with(itemView) {
val collapsedHeight = context.resources.getDimensionPixelSize(R.dimen.quote_collapsed_height) val collapsedHeight = context.resources.getDimensionPixelSize(R.dimen.quote_collapsed_height)
val viewMore = context.getString(R.string.msg_view_more) val viewMore = context.getString(R.string.msg_view_more)
val viewLess = context.getString(R.string.msg_view_less) val viewLess = context.getString(R.string.msg_view_less)
text_message_time.text = data.time text_message_time.text = data.timestamp
text_sender.text = data.senderName text_sender.text = data.authorName
text_content.text = data.content text_content.text = data.text
text_view_more.isVisible = true text_view_more.isVisible = true
text_view_more.text = if (isExpanded())viewLess else viewMore text_view_more.text = if (isExpanded()) viewLess else viewMore
val lp = text_content.layoutParams val lp = text_content.layoutParams
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
text_content.layoutParams = lp text_content.layoutParams = lp
...@@ -49,13 +197,13 @@ class MessageAttachmentViewHolder( ...@@ -49,13 +197,13 @@ class MessageAttachmentViewHolder(
} }
val expandAnimation = ValueAnimator val expandAnimation = ValueAnimator
.ofInt(collapsedHeight, textMeasuredHeight) .ofInt(collapsedHeight, textMeasuredHeight)
.setDuration(300) .setDuration(300)
expandAnimation.interpolator = LinearInterpolator() expandAnimation.interpolator = LinearInterpolator()
val collapseAnimation = ValueAnimator val collapseAnimation = ValueAnimator
.ofInt(textMeasuredHeight, collapsedHeight) .ofInt(textMeasuredHeight, collapsedHeight)
.setDuration(300) .setDuration(300)
collapseAnimation.interpolator = LinearInterpolator() collapseAnimation.interpolator = LinearInterpolator()
expandAnimation.addUpdateListener { expandAnimation.addUpdateListener {
...@@ -98,6 +246,50 @@ class MessageAttachmentViewHolder( ...@@ -98,6 +246,50 @@ class MessageAttachmentViewHolder(
} }
} }
private fun bindAuthorLink(data: AttachmentUiModel) {
with(itemView) {
author_icon.setImageURI(data.authorIcon)
text_author_name.content = data.authorName
text_author_name.setOnClickListener {
openTabbedUrl(data.authorLink)
}
}
}
private fun bindTitleLink(data: AttachmentUiModel) {
with(itemView) {
val filename = data.title ?: data.titleLink
file_name_not_file_type.text = filename
file_name_not_file_type.setOnClickListener {
openTabbedUrl(data.titleLink)
}
}
}
private fun bindFields(data: AttachmentUiModel) {
with(itemView) {
text_fields.content = data.fields
}
}
private fun bindActions(data: AttachmentUiModel) {
val actions = data.actions
val alignment = data.buttonAlignment
Timber.d("no of actions : ${actions!!.size} : $actions")
with(itemView) {
attachment_text.isVisible = data.hasText
attachment_text.text = data.text
actions_list.layoutManager = LinearLayoutManager(itemView.context,
when (alignment) {
"horizontal" -> LinearLayoutManager.HORIZONTAL
else -> LinearLayoutManager.VERTICAL //Default
}, false)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
private fun isExpanded(): Boolean { private fun isExpanded(): Boolean {
with(itemView) { with(itemView) {
val lp = text_content.layoutParams val lp = text_content.layoutParams
...@@ -105,3 +297,7 @@ class MessageAttachmentViewHolder( ...@@ -105,3 +297,7 @@ class MessageAttachmentViewHolder(
} }
} }
} }
interface ActionAttachmentOnClickListener {
fun onActionClicked(view: View, action: Action)
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.AudioAttachmentUiModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.isVisible = false
audio_video_attachment.isVisible = true
}
}
override fun bindViews(data: AudioAttachmentUiModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.AuthorAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.content
import chat.rocket.common.util.ifNull
import kotlinx.android.synthetic.main.item_author_attachment.view.*
class AuthorAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AuthorAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(author_attachment_container)
}
}
override fun bindViews(data: AuthorAttachmentUiModel) {
with(itemView) {
data.icon?.let { icon ->
author_icon.isVisible = true
author_icon.setImageURI(icon)
}.ifNull {
author_icon.isGone = true
}
author_icon.setImageURI(data.icon)
text_author_name.content = data.name
data.fields?.let { fields ->
text_fields.content = fields
text_fields.isVisible = true
}.ifNull {
text_fields.isGone = true
}
text_author_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
}
}
}
}
\ No newline at end of file
...@@ -36,37 +36,13 @@ class ChatRoomAdapter( ...@@ -36,37 +36,13 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_message) val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener, reactionListener) MessageViewHolder(view, actionsListener, reactionListener)
} }
BaseUiModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.URL_PREVIEW -> { BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener) UrlPreviewViewHolder(view, actionsListener, reactionListener)
} }
BaseUiModel.ViewType.MESSAGE_ATTACHMENT -> { BaseUiModel.ViewType.ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment) val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener) AttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
BaseUiModel.ViewType.AUTHOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.GENERIC_FILE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseUiModel.ViewType.MESSAGE_REPLY -> { BaseUiModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply) val view = parent.inflate(R.layout.item_message_reply)
...@@ -74,10 +50,6 @@ class ChatRoomAdapter( ...@@ -74,10 +50,6 @@ class ChatRoomAdapter(
actionSelectListener?.openDirectMessage(roomName, permalink) actionSelectListener?.openDirectMessage(roomName, permalink)
} }
} }
BaseUiModel.ViewType.ACTIONS_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_actions_attachment)
ActionsAttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -113,26 +85,12 @@ class ChatRoomAdapter( ...@@ -113,26 +85,12 @@ class ChatRoomAdapter(
when (holder) { when (holder) {
is MessageViewHolder -> is MessageViewHolder ->
holder.bind(dataSet[position] as MessageUiModel) holder.bind(dataSet[position] as MessageUiModel)
is ImageAttachmentViewHolder ->
holder.bind(dataSet[position] as ImageAttachmentUiModel)
is AudioAttachmentViewHolder ->
holder.bind(dataSet[position] as AudioAttachmentUiModel)
is VideoAttachmentViewHolder ->
holder.bind(dataSet[position] as VideoAttachmentUiModel)
is UrlPreviewViewHolder -> is UrlPreviewViewHolder ->
holder.bind(dataSet[position] as UrlPreviewUiModel) holder.bind(dataSet[position] as UrlPreviewUiModel)
is MessageAttachmentViewHolder ->
holder.bind(dataSet[position] as MessageAttachmentUiModel)
is AuthorAttachmentViewHolder ->
holder.bind(dataSet[position] as AuthorAttachmentUiModel)
is ColorAttachmentViewHolder ->
holder.bind(dataSet[position] as ColorAttachmentUiModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentUiModel)
is MessageReplyViewHolder -> is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel) holder.bind(dataSet[position] as MessageReplyUiModel)
is ActionsAttachmentViewHolder -> is AttachmentViewHolder ->
holder.bind(dataSet[position] as ActionsAttachmentUiModel) holder.bind(dataSet[position] as AttachmentUiModel)
} }
} }
...@@ -140,8 +98,7 @@ class ChatRoomAdapter( ...@@ -140,8 +98,7 @@ class ChatRoomAdapter(
val model = dataSet[position] val model = dataSet[position]
return when (model) { return when (model) {
is MessageUiModel -> model.messageId.hashCode().toLong() is MessageUiModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentUiModel -> model.id is AttachmentUiModel -> model.id
is AuthorAttachmentUiModel -> model.id
else -> return position.toLong() else -> return position.toLong()
} }
} }
...@@ -291,6 +248,9 @@ class ChatRoomAdapter( ...@@ -291,6 +248,9 @@ class ChatRoomAdapter(
R.id.action_menu_msg_react -> { R.id.action_menu_msg_react -> {
actionSelectListener?.showReactions(id) actionSelectListener?.showReactions(id)
} }
R.id.action_message_permalink -> {
actionSelectListener?.copyPermalink(id)
}
else -> { else -> {
TODO("Not implemented") TODO("Not implemented")
} }
...@@ -310,5 +270,6 @@ class ChatRoomAdapter( ...@@ -310,5 +270,6 @@ class ChatRoomAdapter(
fun showReactions(id: String) fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String) fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String) fun sendMessage(chatRoomId: String, text: String)
fun copyPermalink(id: String)
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.ColorAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_color_attachment.view.*
class ColorAttachmentViewHolder(itemView: View,
listener: BaseViewHolder.ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentUiModel>(itemView, listener, reactionListener) {
val drawable: Drawable = ColorDrawable(ContextCompat.getColor(itemView.context, R.color.quoteBar))
init {
with(itemView) {
setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod()
}
}
override fun bindViews(data: ColorAttachmentUiModel) {
with(itemView) {
quote_bar.setColorFilter(data.color)
if (data.text.isNotEmpty()) {
attachment_text.isVisible = true
attachment_text.text = data.text
} else {
attachment_text.isVisible = false
}
if (data.fields.isNullOrEmpty()) {
text_fields.isVisible = false
} else {
text_fields.isVisible = true
text_fields.text = data.fields
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.view.View
import androidx.core.net.toUri
import chat.rocket.android.chatroom.uimodel.GenericFileAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.content
import kotlinx.android.synthetic.main.item_file_attachment.view.*
class GenericFileAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<GenericFileAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(file_attachment_container)
}
}
override fun bindViews(data: GenericFileAttachmentUiModel) {
with(itemView) {
text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, data.attachmentUrl.toUri()))
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ImageAttachmentUiModel
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.emoji.EmojiReactionListener
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
}
}
override fun bindViews(data: ImageAttachmentUiModel) {
with(itemView) {
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(data.attachmentUrl)
autoPlayAnimations = true
oldController = image_attachment.controller
}.build()
image_attachment.controller = controller
file_name.text = data.attachmentTitle
file_description.text = data.attachmentDescription
file_text.text = data.attachmentText
image_attachment.setOnClickListener {
ImageHelper.openImage(
context,
data.attachmentUrl,
data.attachmentTitle.toString()
)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.VideoAttachmentUiModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.isVisible = false
audio_video_attachment.isVisible = true
}
}
override fun bindViews(data: VideoAttachmentUiModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
...@@ -35,8 +35,10 @@ import chat.rocket.android.server.domain.useRealName ...@@ -35,8 +35,10 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extension.compressImageAndGetByteArray import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.getByteArray
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
...@@ -80,6 +82,7 @@ import kotlinx.coroutines.experimental.launch ...@@ -80,6 +82,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant import org.threeten.bp.Instant
import timber.log.Timber import timber.log.Timber
import java.io.InputStream
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
...@@ -135,9 +138,12 @@ class ChatRoomPresenter @Inject constructor( ...@@ -135,9 +138,12 @@ class ChatRoomPresenter @Inject constructor(
} finally { } finally {
// User has at least an 'owner' or 'moderator' role. // User has at least an 'owner' or 'moderator' role.
val userCanMod = isOwnerOrMod() val userCanMod = isOwnerOrMod()
val chatRoom = dbManager.getRoom(roomId)
val muted = chatRoom?.chatRoom?.muted ?: emptyList()
// Can post anyway if has the 'post-readonly' permission on server. // Can post anyway if has the 'post-readonly' permission on server.
val userCanPost = userCanMod || permissions.canPostToReadOnlyChannels() val userCanPost = userCanMod || permissions.canPostToReadOnlyChannels() ||
chatIsBroadcast = dbManager.getRoom(roomId)?.chatRoom?.run { !muted.contains(currentLoggedUsername)
chatIsBroadcast = chatRoom?.chatRoom?.run {
broadcast broadcast
} ?: false } ?: false
view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod) view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod)
...@@ -174,7 +180,9 @@ class ChatRoomPresenter @Inject constructor( ...@@ -174,7 +180,9 @@ class ChatRoomPresenter @Inject constructor(
view.showLoading() view.showLoading()
try { try {
if (offset == 0L) { if (offset == 0L) {
val localMessages = messagesRepository.getByRoomId(chatRoomId) // FIXME - load just 50 messages from DB to speed up. We will reload from Network after that
// FIXME - We need to handle the pagination, first fetch from DB, then from network
val localMessages = messagesRepository.getRecentMessages(chatRoomId, 50)
val oldMessages = mapper.map( val oldMessages = mapper.map(
localMessages, RoomUiModel( localMessages, RoomUiModel(
roles = chatRoles, roles = chatRoles,
...@@ -346,15 +354,51 @@ class ChatRoomPresenter @Inject constructor( ...@@ -346,15 +354,51 @@ class ChatRoomPresenter @Inject constructor(
view.showFileSelection(settings.uploadMimeTypeFilter()) view.showFileSelection(settings.uploadMimeTypeFilter())
} }
fun uploadFile(roomId: String, uri: Uri, msg: String, bitmap: Bitmap? = null) { fun uploadImage(roomId: String, mimeType: String, uri: Uri, bitmap: Bitmap, msg: String) {
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
try { try {
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString() val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val mimeType = uriInteractor.getMimeType(uri) if (fileName.isEmpty()) {
val byteArray = bitmap?.compressImageAndGetByteArray(mimeType) view.showInvalidFileMessage()
val fileSize = byteArray?.size ?: uriInteractor.getFileSize(uri) } else {
val byteArray =
bitmap.getByteArray(mimeType, 100, settings.uploadMaxFileSize())
retryIO("uploadFile($roomId, $fileName, $mimeType") {
client.uploadFile(
roomId,
fileName,
mimeType,
msg,
description = fileName
) {
byteArray.inputStream()
}
}
logMediaUploaded(mimeType)
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error uploading image")
when (ex) {
is RocketChatException -> view.showMessage(ex)
else -> view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun uploadFile(roomId: String, mimeType: String, uri: Uri, msg: String) {
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val fileSize = uriInteractor.getFileSize(uri)
val maxFileSizeAllowed = settings.uploadMaxFileSize() val maxFileSizeAllowed = settings.uploadMaxFileSize()
when { when {
...@@ -370,7 +414,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -370,7 +414,7 @@ class ChatRoomPresenter @Inject constructor(
msg, msg,
description = fileName description = fileName
) { ) {
byteArray?.inputStream() ?: uriInteractor.getInputStream(uri) uriInteractor.getInputStream(uri)
} }
} }
logMediaUploaded(mimeType) logMediaUploaded(mimeType)
...@@ -503,7 +547,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -503,7 +547,7 @@ class ChatRoomPresenter @Inject constructor(
val messages = val messages =
retryIO(description = "history($chatRoomId, $roomType, $instant)") { retryIO(description = "history($chatRoomId, $roomType, $instant)") {
client.history( client.history(
chatRoomId, roomType, count = 50, chatRoomId, roomType, count = 50,
oldest = instant oldest = instant
) )
} }
...@@ -620,6 +664,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -620,6 +664,7 @@ class ChatRoomPresenter @Inject constructor(
try { try {
messagesRepository.getById(messageId)?.let { m -> messagesRepository.getById(messageId)?.let { m ->
view.copyToClipboard(m.message) view.copyToClipboard(m.message)
view.showMessage(R.string.msg_message_copied)
} }
} catch (e: RocketChatException) { } catch (e: RocketChatException) {
Timber.e(e) Timber.e(e)
...@@ -857,6 +902,42 @@ class ChatRoomPresenter @Inject constructor( ...@@ -857,6 +902,42 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) {
return@withContext dbManager.chatRoomDao().get(roomId)?.let {
with(it.chatRoom) {
ChatRoom(
id = id,
subscriptionId = subscriptionId,
type = roomTypeOf(type),
unread = unread,
broadcast = broadcast ?: false,
alert = alert,
fullName = fullname,
name = name,
favorite = favorite ?: false,
default = isDefault ?: false,
readonly = readonly,
open = open,
lastMessage = null,
archived = false,
status = null,
user = null,
userMentions = userMentions,
client = client,
announcement = null,
description = null,
groupMentions = groupMentions,
roles = null,
topic = null,
lastSeen = this.lastSeen,
timestamp = timestamp,
updatedAt = updatedAt
)
}
}
}
// TODO: move this to new interactor or FetchChatRoomsInteractor? // TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomsAsync(name: String? = null): List<ChatRoom> = withContext(CommonPool) { private suspend fun getChatRoomsAsync(name: String? = null): List<ChatRoom> = withContext(CommonPool) {
return@withContext dbManager.chatRoomDao().getAllSync().filter { return@withContext dbManager.chatRoomDao().getAllSync().filter {
...@@ -939,6 +1020,24 @@ class ChatRoomPresenter @Inject constructor( ...@@ -939,6 +1020,24 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
fun copyPermalink(messageId: String) {
launchUI(strategy) {
try {
messagesRepository.getById(messageId)?.let { message ->
getChatRoomAsync(message.roomId)?.let { chatRoom ->
val models = mapper.map(message)
models.firstOrNull()?.permalink?.let {
view.copyToClipboard(it)
view.showMessage(R.string.msg_permalink_copied)
}
}
}
} catch (ex: Exception) {
Timber.e(ex)
}
}
}
/** /**
* Send an emoji reaction to a message. * Send an emoji reaction to a message.
*/ */
...@@ -1074,24 +1173,26 @@ class ChatRoomPresenter @Inject constructor( ...@@ -1074,24 +1173,26 @@ class ChatRoomPresenter @Inject constructor(
} }
private fun processTypingStatus(typingStatus: Pair<String, Boolean>) { private fun processTypingStatus(typingStatus: Pair<String, Boolean>) {
if (typingStatus.first != currentLoggedUsername) { synchronized(typingStatusList) {
if (!typingStatusList.any { username -> username == typingStatus.first }) { if (typingStatus.first != currentLoggedUsername) {
if (typingStatus.second) { if (!typingStatusList.any { username -> username == typingStatus.first }) {
typingStatusList.add(typingStatus.first)
}
} else {
typingStatusList.find { username -> username == typingStatus.first }?.let {
typingStatusList.remove(it)
if (typingStatus.second) { if (typingStatus.second) {
typingStatusList.add(typingStatus.first) typingStatusList.add(typingStatus.first)
} }
} else {
typingStatusList.find { username -> username == typingStatus.first }?.let {
typingStatusList.remove(it)
if (typingStatus.second) {
typingStatusList.add(typingStatus.first)
}
}
} }
}
if (typingStatusList.isNotEmpty()) { if (typingStatusList.isNotEmpty()) {
view.showTypingStatus(typingStatusList.toList()) view.showTypingStatus(typingStatusList.toList())
} else { } else {
view.hideTypingStatusView() view.hideTypingStatusView()
}
} }
} }
} }
......
...@@ -545,6 +545,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -545,6 +545,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun dispatchUpdateMessage(index: Int, message: List<BaseUiModel<*>>) { override fun dispatchUpdateMessage(index: Int, message: List<BaseUiModel<*>>) {
ui { ui {
// TODO - investigate WHY we get a empty list here
if (message.isEmpty()) return@ui
if (adapter.updateItem(message.last())) { if (adapter.updateItem(message.last())) {
if (message.size > 1) { if (message.size > 1) {
adapter.prependData(listOf(message.first())) adapter.prependData(listOf(message.first()))
...@@ -591,7 +594,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -591,7 +594,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage(){
ui {
showMessage(getString(R.string.msg_generic_error))
}
}
override fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>) { override fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>) {
ui { ui {
...@@ -621,7 +628,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -621,7 +628,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
ui { ui {
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", message) clipboard.primaryClip = ClipData.newPlainText("", message)
showToast(R.string.msg_message_copied)
} }
} }
...@@ -764,9 +770,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -764,9 +770,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
private fun setupMessageComposer(canPost: Boolean) { private fun setupMessageComposer(canPost: Boolean) {
if (isReadOnly && !canPost) { if (!canPost) {
text_room_is_read_only.isVisible = true text_room_is_read_only.isVisible = true
input_container.isVisible = false input_container.isVisible = false
text_room_is_read_only.setText(
if (isReadOnly) {
R.string.msg_this_room_is_read_only
} else {
// Not a read-only channel but user has been muted.
R.string.msg_muted_on_this_channel
}
)
} else if (!isSubscribed && roomTypeOf(chatRoomType) !is RoomType.DirectMessage) { } else if (!isSubscribed && roomTypeOf(chatRoomType) !is RoomType.DirectMessage) {
input_container.isVisible = false input_container.isVisible = false
button_join_chat.isVisible = true button_join_chat.isVisible = true
...@@ -1059,6 +1073,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -1059,6 +1073,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun copyPermalink(id: String) {
presenter.copyPermalink(id)
}
override fun showReactions(id: String) { override fun showReactions(id: String) {
presenter.showReactions(id) presenter.showReactions(id)
} }
......
...@@ -7,7 +7,7 @@ import androidx.core.view.isVisible ...@@ -7,7 +7,7 @@ import androidx.core.view.isVisible
import chat.rocket.android.emoji.internal.GlideApp import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.util.extensions.getFileName import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType import chat.rocket.android.util.extensions.getMimeType
import com.bumptech.glide.load.resource.gif.GifDrawable import chat.rocket.common.util.ifNull
import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
...@@ -15,10 +15,12 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) { ...@@ -15,10 +15,12 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
imagePreview.isVisible = false imagePreview.isVisible = false
audioVideoAttachment.isVisible = false audioVideoAttachment.isVisible = false
textFile.isVisible = false textFile.isVisible = false
lateinit var mimeType: String
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
activity?.let { context -> activity?.let { context ->
uri.getMimeType(context).let { mimeType -> uri.getMimeType(context).let {
mimeType = it
description.text.clear() description.text.clear()
when { when {
mimeType.startsWith("image") -> { mimeType.startsWith("image") -> {
...@@ -27,7 +29,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) { ...@@ -27,7 +29,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
.with(context) .with(context)
.asGif() .asGif()
.load(uri) .load(uri)
.override(imagePreview.width, imagePreview.height)
.fitCenter() .fitCenter()
.into(imagePreview) .into(imagePreview)
} else { } else {
...@@ -35,7 +36,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) { ...@@ -35,7 +36,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
.with(context) .with(context)
.asBitmap() .asBitmap()
.load(uri) .load(uri)
.override(imagePreview.width, imagePreview.height)
.fitCenter() .fitCenter()
.into(object : SimpleTarget<Bitmap>() { .into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady( override fun onResourceReady(
...@@ -59,12 +59,22 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) { ...@@ -59,12 +59,22 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
} }
sendButton.setOnClickListener { sendButton.setOnClickListener {
presenter.uploadFile( bitmap?.let { bitmap ->
chatRoomId, presenter.uploadImage(
uri, chatRoomId,
(citation ?: "") + description.text.toString(), mimeType,
bitmap uri,
) bitmap,
(citation ?: "") + description.text.toString()
)
}.ifNull {
presenter.uploadFile(
chatRoomId,
mimeType,
uri,
(citation ?: "") + description.text.toString()
)
}
alertDialog.dismiss() alertDialog.dismiss()
} }
cancelButton.setOnClickListener { alertDialog.dismiss() } cancelButton.setOnClickListener { alertDialog.dismiss() }
......
...@@ -73,4 +73,4 @@ class MessageActionsBottomSheet : BottomSheetDialogFragment() { ...@@ -73,4 +73,4 @@ class MessageActionsBottomSheet : BottomSheetDialogFragment() {
} }
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
data class ActionsAttachmentUiModel(
override val attachmentUrl: String,
val title: String?,
val actions: List<Action>,
val buttonAlignment: String,
override val message: Message,
override val rawData: ActionsAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ActionsAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ACTIONS_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_actions_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.actions.Action
data class AttachmentUiModel(
override val message: Message,
override val rawData: Attachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message?,
override var isTemporary: Boolean,
override var unread: Boolean?,
override var currentDayMarkerText: String,
override var showDayMarker: Boolean,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String,
val id: Long,
val title: CharSequence?,
val description: CharSequence?,
val authorName: CharSequence?,
val text: CharSequence?,
val color: Int?,
val imageUrl: String?,
val videoUrl: String?,
val audioUrl: String?,
val titleLink: String?,
val messageLink: String?,
val type: String?,
// TODO - attachments
val timestamp: CharSequence?,
val authorIcon: String?,
val authorLink: String?,
val fields: CharSequence?,
val buttonAlignment: String?,
val actions: List<Action>?
) : BaseUiModel<Attachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
val hasTitle: Boolean
get() = !title.isNullOrEmpty()
val hasDescription: Boolean
get() = !description.isNullOrEmpty()
val hasText: Boolean
get() = !text.isNullOrEmpty()
val hasImage: Boolean
get() = imageUrl.orEmpty().isNotEmpty()
val hasVideo: Boolean
get() = videoUrl.orEmpty().isNotEmpty()
val hasAudio: Boolean
get() = audioUrl.orEmpty().isNotEmpty()
val hasAudioOrVideo: Boolean
get() = hasAudio || hasVideo
val hasFile: Boolean
get() = type.orEmpty().contentEquals("file") && titleLink.orEmpty().isNotEmpty()
val hasTitleLink: Boolean
get() = titleLink.orEmpty().isNotEmpty()
val hasMedia: Boolean
get() = hasImage || hasAudioOrVideo || hasFile
val hasMessage: Boolean
get() = messageLink.orEmpty().isNotEmpty()
val hasAuthorName: Boolean
get() = !authorName.isNullOrEmpty()
val hasAuthorLink: Boolean
get() = authorLink.orEmpty().isNotEmpty()
val hasAuthorIcon: Boolean
get() = authorIcon.orEmpty().isNotEmpty()
val hasFields: Boolean
get() = !fields.isNullOrEmpty()
val hasActions: Boolean
get() = actions != null && actions.isNotEmpty()
}
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AudioAttachment
data class AudioAttachmentUiModel(
override val message: Message,
override val rawData: AudioAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<AudioAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AuthorAttachment
data class AuthorAttachmentUiModel(
override val attachmentUrl: String,
val id: Long,
val name: CharSequence?,
val icon: String?,
val fields: CharSequence?,
override val message: Message,
override val rawData: AuthorAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<AuthorAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.AUTHOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_author_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
interface BaseFileAttachmentUiModel<out T> : BaseAttachmentUiModel<T> {
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
...@@ -17,20 +17,14 @@ interface BaseUiModel<out T> { ...@@ -17,20 +17,14 @@ interface BaseUiModel<out T> {
var currentDayMarkerText: String var currentDayMarkerText: String
var showDayMarker: Boolean var showDayMarker: Boolean
var menuItemsToHide: MutableList<Int> var menuItemsToHide: MutableList<Int>
var permalink: String
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
SYSTEM_MESSAGE(1), SYSTEM_MESSAGE(1),
URL_PREVIEW(2), URL_PREVIEW(2),
IMAGE_ATTACHMENT(3), ATTACHMENT(3),
VIDEO_ATTACHMENT(4), MESSAGE_REPLY(4)
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10),
ACTIONS_ATTACHMENT(11)
} }
} }
......
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ColorAttachment
data class ColorAttachmentUiModel(
override val attachmentUrl: String,
val id: Long,
val color: Int,
val text: CharSequence,
val fields: CharSequence? = null,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean?,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ColorAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.COLOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_color_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.GenericFileAttachment
data class GenericFileAttachmentUiModel(
override val message: Message,
override val rawData: GenericFileAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<GenericFileAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.GENERIC_FILE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_file_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ImageAttachment
data class ImageAttachmentUiModel(
override val message: Message,
override val rawData: ImageAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
val attachmentText: String?,
val attachmentDescription: String?,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<ImageAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.IMAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentUiModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String?,
val time: CharSequence?,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
...@@ -15,7 +15,8 @@ data class MessageReplyUiModel( ...@@ -15,7 +15,8 @@ data class MessageReplyUiModel(
override var unread: Boolean? = null, override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(), override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String, override var currentDayMarkerText: String,
override var showDayMarker: Boolean override var showDayMarker: Boolean,
override var permalink: String
) : BaseUiModel<MessageReply> { ) : BaseUiModel<MessageReply> {
override val viewType: Int override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE_REPLY.viewType get() = BaseUiModel.ViewType.MESSAGE_REPLY.viewType
......
...@@ -20,7 +20,8 @@ data class MessageUiModel( ...@@ -20,7 +20,8 @@ data class MessageUiModel(
override var unread: Boolean? = null, override var unread: Boolean? = null,
var isFirstUnread: Boolean, var isFirstUnread: Boolean,
override var isTemporary: Boolean = false, override var isTemporary: Boolean = false,
override var menuItemsToHide: MutableList<Int> = mutableListOf() override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String
) : BaseMessageUiModel<Message> { ) : BaseMessageUiModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType get() = BaseUiModel.ViewType.MESSAGE.viewType
......
...@@ -32,6 +32,7 @@ import chat.rocket.android.server.domain.messageReadReceiptStoreUsers ...@@ -32,6 +32,7 @@ import chat.rocket.android.server.domain.messageReadReceiptStoreUsers
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.ifNotNullNorEmpty
import chat.rocket.android.util.extensions.isNotNullNorEmpty import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
...@@ -39,16 +40,7 @@ import chat.rocket.core.model.Message ...@@ -39,16 +40,7 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType import chat.rocket.core.model.MessageType
import chat.rocket.core.model.ReadReceipt import chat.rocket.core.model.ReadReceipt
import chat.rocket.core.model.attachment.Attachment import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.Field import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.isSystemMessage import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
...@@ -131,23 +123,26 @@ class UiModelMapper @Inject constructor( ...@@ -131,23 +123,26 @@ class UiModelMapper @Inject constructor(
withContext(CommonPool) { withContext(CommonPool) {
val list = ArrayList<BaseUiModel<*>>() val list = ArrayList<BaseUiModel<*>>()
message.urls?.forEach { url -> getChatRoomAsync(message.roomId)?.let { chatRoom ->
mapUrl(message, url)?.let { list.add(it) } message.urls?.forEach { url ->
} mapUrl(message, url, chatRoom)?.let { list.add(it) }
}
message.attachments?.mapNotNull { attachment -> message.attachments?.mapNotNull { attachment ->
mapAttachment(message, attachment) mapAttachment(message, attachment, chatRoom)
}?.asReversed()?.let { }?.asReversed()?.let {
list.addAll(it) list.addAll(it)
} }
mapMessage(message).let { mapMessage(message, chatRoom).let {
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
it.preview = list.first().preview it.preview = list.first().preview
}
list.add(it)
} }
list.add(it)
} }
for (i in list.size - 1 downTo 0) { for (i in list.size - 1 downTo 0) {
val next = if (i - 1 < 0) null else list[i - 1] val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next list[i].nextDownStreamMessage = next
...@@ -214,24 +209,26 @@ class UiModelMapper @Inject constructor( ...@@ -214,24 +209,26 @@ class UiModelMapper @Inject constructor(
withContext(CommonPool) { withContext(CommonPool) {
val list = ArrayList<BaseUiModel<*>>() val list = ArrayList<BaseUiModel<*>>()
mapMessage(message).let { getChatRoomAsync(message.roomId)?.let { chatRoom ->
if (list.isNotEmpty()) { mapMessage(message, chatRoom).let {
it.preview = list.first().preview if (list.isNotEmpty()) {
it.preview = list.first().preview
}
list.add(it)
} }
list.add(it)
}
message.attachments?.forEach { message.attachments?.forEach {
val attachment = mapAttachment(message, it) val attachment = mapAttachment(message, it, chatRoom)
attachment?.let { attachment?.let {
list.add(attachment) list.add(attachment)
}
} }
}
message.urls?.forEach { message.urls?.forEach {
val url = mapUrl(message, it) val url = mapUrl(message, it, chatRoom)
url?.let { url?.let {
list.add(url) list.add(url)
}
} }
} }
...@@ -283,11 +280,12 @@ class UiModelMapper @Inject constructor( ...@@ -283,11 +280,12 @@ class UiModelMapper @Inject constructor(
nextDownStreamMessage = null, nextDownStreamMessage = null,
unread = message.unread, unread = message.unread,
currentDayMarkerText = dayMarkerText, currentDayMarkerText = dayMarkerText,
showDayMarker = false showDayMarker = false,
permalink = messageHelper.createPermalink(message, chatRoom, false)
) )
} }
private fun mapUrl(message: Message, url: Url): BaseUiModel<*>? { private fun mapUrl(message: Message, url: Url, chatRoom: ChatRoom): BaseUiModel<*>? {
if (url.ignoreParse || url.meta == null) return null if (url.ignoreParse || url.meta == null) return null
val hostname = url.parsedUrl?.hostname ?: "" val hostname = url.parsedUrl?.hostname ?: ""
...@@ -297,39 +295,14 @@ class UiModelMapper @Inject constructor( ...@@ -297,39 +295,14 @@ class UiModelMapper @Inject constructor(
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp) val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context) val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
val permalink = messageHelper.createPermalink(message, chatRoom, false)
return UrlPreviewUiModel(message, url, message.id, title, hostname, description, thumb, return UrlPreviewUiModel(message, url, message.id, title, hostname, description, thumb,
getReactions(message), preview = message.copy(message = url.url), unread = message.unread, getReactions(message), preview = message.copy(message = url.url), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText) showDayMarker = false, currentDayMarkerText = dayMarkerText, permalink = permalink)
}
private fun mapAttachment(message: Message, attachment: Attachment): BaseUiModel<*>? {
return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
is ActionsAttachment -> mapActionsAttachment(message, attachment)
else -> null
}
} }
private fun mapActionsAttachment(message: Message, attachment: ActionsAttachment): BaseUiModel<*>? { private fun mapAttachment(message: Message, attachment: Attachment, chatRoom: ChatRoom): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
ActionsAttachmentUiModel(attachmentUrl = url, title = title,
actions = actions, buttonAlignment = buttonAlignment, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}
private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseUiModel<*>? {
return with(attachment) { return with(attachment) {
val content = stripMessageQuotes(message) val content = stripMessageQuotes(message)
val id = attachmentId(message, attachment) val id = attachmentId(message, attachment)
...@@ -337,12 +310,51 @@ class UiModelMapper @Inject constructor( ...@@ -337,12 +310,51 @@ class UiModelMapper @Inject constructor(
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp) val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context) val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
val fieldsText = mapFields(fields) val fieldsText = mapFields(fields)
val permalink = messageHelper.createPermalink(message, chatRoom, false)
ColorAttachmentUiModel(attachmentUrl = url, id = id, color = color.color,
text = text, fields = fieldsText, message = message, rawData = attachment, val attachmentAuthor = attachment.authorName
messageId = message.id, reactions = getReactions(message), val time = attachment.timestamp?.let { getTime(it) }
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText) val imageUrl = attachmentUrl(attachment.imageUrl)
val videoUrl = attachmentUrl(attachment.videoUrl)
val audioUrl = attachmentUrl(attachment.audioUrl)
val titleLink = attachmentUrl(attachment.titleLink)
val attachmentTitle = attachmentTitle(attachment.title, imageUrl, videoUrl, audioUrl, titleLink)
val attachmentText = attachmentText(attachment.text, attachment.attachments?.firstOrNull(), context)
val attachmentDescription = attachmentDescription(attachment)
AttachmentUiModel(
message = message,
rawData = this,
messageId = message.id,
reactions = getReactions(message),
preview = message.copy(message = content.message),
isTemporary = !message.synced,
unread = message.unread,
currentDayMarkerText = dayMarkerText,
showDayMarker = false,
permalink = permalink,
id = id,
title = attachmentTitle,
description = attachmentDescription,
authorName = attachmentAuthor,
text = attachmentText,
color = color?.color,
imageUrl = imageUrl,
videoUrl = videoUrl,
audioUrl = audioUrl,
titleLink = titleLink,
type = type,
messageLink = messageLink,
timestamp = time,
authorIcon = authorIcon,
authorLink = authorLink,
fields = fieldsText,
buttonAlignment = buttonAlignment,
actions = actions
)
} }
} }
...@@ -364,122 +376,64 @@ class UiModelMapper @Inject constructor( ...@@ -364,122 +376,64 @@ class UiModelMapper @Inject constructor(
} }
} }
private fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentUiModel { private fun attachmentId(message: Message, attachment: Attachment): Long {
return with(attachment) { return "${message.id}_${attachment.hashCode()}".hashCode().toLong()
val content = stripMessageQuotes(message)
val fieldsText = mapFields(fields)
val id = attachmentId(message, attachment)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
AuthorAttachmentUiModel(attachmentUrl = url, id = id, name = authorName,
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}
private fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentUiModel {
val attachmentAuthor = attachment.author
val time = attachment.timestamp?.let { getTime(it) }
val attachmentText = when (attachment.attachments.orEmpty().firstOrNull()) {
is ImageAttachment -> context.getString(R.string.msg_preview_photo)
is VideoAttachment -> context.getString(R.string.msg_preview_video)
is AudioAttachment -> context.getString(R.string.msg_preview_audio)
is GenericFileAttachment -> context.getString(R.string.msg_preview_file)
else -> attachment.text ?: ""
}
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
val content = stripMessageQuotes(message)
return MessageAttachmentUiModel(message = content, rawData = message,
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
currentDayMarkerText = dayMarkerText, showDayMarker = false)
} }
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseUiModel<*>? { private fun attachmentTitle(title: String?, vararg url: String?): CharSequence {
val attachmentUrl = attachmentUrl(attachment) title?.let { return it }
val attachmentTitle = attachmentTitle(attachment)
val attachmentText = attachmentText(attachment)
val attachmentDescription = attachmentDescription(attachment)
val id = attachmentId(message, attachment)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
return when (attachment) { url.filterNotNull().forEach {
is ImageAttachment -> ImageAttachmentUiModel(message, attachment, message.id, val fileUrl = HttpUrl.parse(it)
attachmentUrl, attachmentTitle, attachmentText, attachmentDescription, id, getReactions(message), fileUrl?.let { httpUrl ->
preview = message.copy(message = context.getString(R.string.msg_preview_photo)), unread = message.unread, return httpUrl.pathSegments().last()
showDayMarker = false, currentDayMarkerText = dayMarkerText) }
is VideoAttachment -> VideoAttachmentUiModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_video)), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
is AudioAttachment -> AudioAttachmentUiModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_audio)), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
is GenericFileAttachment -> GenericFileAttachmentUiModel(message, attachment,
message.id, attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_file)), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
else -> null
} }
}
private fun attachmentId(message: Message, attachment: Attachment): Long { return ""
return "${message.id}_${attachment.url}".hashCode().toLong()
} }
private fun attachmentTitle(attachment: FileAttachment): CharSequence { private fun attachmentUrl(url: String?): String? {
return with(attachment) { if (url.isNullOrEmpty()) return null
title?.let { return@with it } if (url!!.startsWith("http")) return url
val fileUrl = HttpUrl.parse(url) val fullUrl = "$baseUrl$url"
fileUrl?.let { val httpUrl = HttpUrl.parse(fullUrl)
return@with it.pathSegments().last() httpUrl?.let {
} return it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
return@with "" addQueryParameter("rc_token", token?.authToken)
}.build().toString()
} }
// Fallback to baseUrl + url
return fullUrl
} }
private fun attachmentUrl(attachment: FileAttachment): String { private fun attachmentText(text: String?, attachment: Attachment?, context: Context): String? {
return with(attachment) { return if (attachment != null) {
if (url.startsWith("http")) return@with url when {
attachment.imageUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_photo)
val fullUrl = "$baseUrl$url" attachment.videoUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_video)
val httpUrl = HttpUrl.parse(fullUrl) attachment.audioUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_audio)
httpUrl?.let { attachment.titleLink.isNotNullNorEmpty() &&
return@with it.newBuilder().apply { attachment.type?.contentEquals("file") == true ->
addQueryParameter("rc_uid", token?.userId) context.getString(R.string.msg_preview_file)
addQueryParameter("rc_token", token?.authToken) else -> text
}.build().toString()
} }
} else {
// Fallback to baseUrl + url text
return@with fullUrl
} }
} }
private fun attachmentText(attachment: FileAttachment): String? { private fun attachmentDescription(attachment: Attachment): String? {
return attachment.text
}
private fun attachmentDescription(attachment: FileAttachment): String? {
return attachment.description return attachment.description
} }
private suspend fun mapMessage(message: Message): MessageUiModel = withContext(CommonPool) { private suspend fun mapMessage(
message: Message,
chatRoom: ChatRoom
): MessageUiModel = withContext(CommonPool) {
val sender = getSenderName(message) val sender = getSenderName(message)
val time = getTime(message.timestamp) val time = getTime(message.timestamp)
val avatar = getUserAvatar(message) val avatar = getUserAvatar(message)
...@@ -493,13 +447,14 @@ class UiModelMapper @Inject constructor( ...@@ -493,13 +447,14 @@ class UiModelMapper @Inject constructor(
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp) val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context) val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
val permalink = messageHelper.createPermalink(message, chatRoom, false)
val content = getContent(stripMessageQuotes(message)) val content = getContent(stripMessageQuotes(message))
MessageUiModel(message = stripMessageQuotes(message), rawData = message, MessageUiModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText, content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText,
showDayMarker = false, reactions = getReactions(message), isFirstUnread = false, showDayMarker = false, reactions = getReactions(message), isFirstUnread = false,
preview = preview, isTemporary = !synced, unread = unread) preview = preview, isTemporary = !synced, unread = unread, permalink = permalink)
} }
private fun mapMessagePreview(message: Message): Message { private fun mapMessagePreview(message: Message): Message {
...@@ -540,7 +495,7 @@ class UiModelMapper @Inject constructor( ...@@ -540,7 +495,7 @@ class UiModelMapper @Inject constructor(
private fun getSenderName(message: Message): CharSequence { private fun getSenderName(message: Message): CharSequence {
val username = message.sender?.username val username = message.sender?.username
message.senderAlias.isNotNullNorEmpty { alias -> message.senderAlias.ifNotNullNorEmpty { alias ->
return buildSpannedString { return buildSpannedString {
append(alias) append(alias)
username?.let { username?.let {
......
...@@ -19,7 +19,8 @@ data class UrlPreviewUiModel( ...@@ -19,7 +19,8 @@ data class UrlPreviewUiModel(
override var unread: Boolean? = null, override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(), override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String, override var currentDayMarkerText: String,
override var showDayMarker: Boolean override var showDayMarker: Boolean,
override var permalink: String
) : BaseUiModel<Url> { ) : BaseUiModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseUiModel.ViewType.URL_PREVIEW.viewType get() = BaseUiModel.ViewType.URL_PREVIEW.viewType
......
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.VideoAttachment
data class VideoAttachmentUiModel(
override val message: Message,
override val rawData: VideoAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<VideoAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.VIDEO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
...@@ -2,7 +2,9 @@ package chat.rocket.android.chatroom.uimodel.suggestion ...@@ -2,7 +2,9 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel import chat.rocket.android.suggestions.model.SuggestionModel
class ChatRoomSuggestionUiModel(text: String, class ChatRoomSuggestionUiModel(
val fullName: String, text: String,
val name: String, val fullName: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) val name: String,
\ No newline at end of file searchList: List<String>
) : SuggestionModel(text, searchList, false)
...@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.uimodel.suggestion ...@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel import chat.rocket.android.suggestions.model.SuggestionModel
class CommandSuggestionUiModel(text: String, class CommandSuggestionUiModel(
val description: String, text: String,
searchList: List<String>) : SuggestionModel(text, searchList) val description: String,
\ No newline at end of file searchList: List<String>
) : SuggestionModel(text, searchList)
\ No newline at end of file
...@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.uimodel.suggestion ...@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
class PeopleSuggestionUiModel(val imageUri: String?, class PeopleSuggestionUiModel(
text: String, val imageUri: String?,
val username: String, text: String,
val name: String, val username: String,
val status: UserStatus?, val name: String,
pinned: Boolean = false, val status: UserStatus?,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) { pinned: Boolean = false,
searchList: List<String>
) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String { override fun toString(): String {
return "PeopleSuggestionUiModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)" return "PeopleSuggestionUiModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
......
...@@ -24,6 +24,7 @@ import chat.rocket.common.model.roomTypeOf ...@@ -24,6 +24,7 @@ import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.model.userStatusOf import chat.rocket.common.model.userStatusOf
import chat.rocket.core.model.Room import chat.rocket.core.model.Room
import chat.rocket.core.model.SpotlightResult import chat.rocket.core.model.SpotlightResult
import ru.noties.markwon.Markwon
class RoomUiModelMapper( class RoomUiModelMapper(
private val context: Application, private val context: Application,
...@@ -119,6 +120,8 @@ class RoomUiModelMapper( ...@@ -119,6 +120,8 @@ class RoomUiModelMapper(
type is RoomType.DirectMessage) } else { null } type is RoomType.DirectMessage) } else { null }
val open = open val open = open
val lastMessageMarkdown = lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
RoomUiModel( RoomUiModel(
id = id, id = id,
name = roomName, name = roomName,
...@@ -128,7 +131,7 @@ class RoomUiModelMapper( ...@@ -128,7 +131,7 @@ class RoomUiModelMapper(
date = timestamp, date = timestamp,
unread = unread, unread = unread,
alert = isUnread, alert = isUnread,
lastMessage = lastMessage, lastMessage = lastMessageMarkdown,
status = status, status = status,
username = if (type is RoomType.DirectMessage) name else null username = if (type is RoomType.DirectMessage) name else null
) )
......
...@@ -33,7 +33,7 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit ...@@ -33,7 +33,7 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
if (room.lastMessage != null) { if (room.lastMessage != null) {
text_last_message.isVisible = true text_last_message.isVisible = true
text_last_message.text = Markwon.markdown(context, room.lastMessage.toString()).toString() text_last_message.text = room.lastMessage
} else { } else {
text_last_message.isGone = true text_last_message.isGone = true
} }
......
...@@ -19,17 +19,21 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A ...@@ -19,17 +19,21 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
if (viewType == VIEW_TYPE_ROOM) { return when (viewType) {
val view = parent.inflate(R.layout.item_chat) VIEW_TYPE_ROOM -> {
return RoomViewHolder(view, listener) val view = parent.inflate(R.layout.item_chat)
} else if (viewType == VIEW_TYPE_HEADER) { RoomViewHolder(view, listener)
val view = parent.inflate(R.layout.item_chatroom_header) }
return HeaderViewHolder(view) VIEW_TYPE_HEADER -> {
} else if (viewType == VIEW_TYPE_LOADING) { val view = parent.inflate(R.layout.item_chatroom_header)
val view = parent.inflate(R.layout.item_loading) HeaderViewHolder(view)
return LoadingViewHolder(view) }
VIEW_TYPE_LOADING -> {
val view = parent.inflate(R.layout.item_loading)
LoadingViewHolder(view)
}
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
throw IllegalStateException("View type must be either Room, Header or Loading")
} }
override fun getItemCount() = values.size override fun getItemCount() = values.size
......
...@@ -14,5 +14,6 @@ data class RoomUiModel( ...@@ -14,5 +14,6 @@ data class RoomUiModel(
val alert: Boolean = false, val alert: Boolean = false,
val lastMessage: CharSequence? = null, val lastMessage: CharSequence? = null,
val status: UserStatus? = null, val status: UserStatus? = null,
val username: String? = null val username: String? = null,
) val muted: List<String> = emptyList()
\ No newline at end of file )
...@@ -56,7 +56,8 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -56,7 +56,8 @@ class ChatRoomsPresenter @Inject constructor(
type = type.toString(), type = type.toString(),
name = username ?: name.toString(), name = username ?: name.toString(),
fullname = name.toString(), fullname = name.toString(),
open = open open = open,
muted = muted
) )
loadChatRoom(entity, false) loadChatRoom(entity, false)
} }
......
...@@ -27,7 +27,7 @@ import kotlinx.coroutines.experimental.launch ...@@ -27,7 +27,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.lang.IllegalArgumentException
import kotlin.coroutines.experimental.coroutineContext import kotlin.coroutines.experimental.coroutineContext
...@@ -171,6 +171,6 @@ fun Query.asSortingOrder(): ChatRoomsRepository.Order { ...@@ -171,6 +171,6 @@ fun Query.asSortingOrder(): ChatRoomsRepository.Order {
ChatRoomsRepository.Order.ACTIVITY ChatRoomsRepository.Order.ACTIVITY
} }
} }
else -> throw InvalidParameterException("Should be ByName or ByActivity") else -> throw IllegalArgumentException("Should be ByName or ByActivity")
} }
} }
...@@ -9,11 +9,13 @@ import chat.rocket.android.db.model.MessageChannels ...@@ -9,11 +9,13 @@ import chat.rocket.android.db.model.MessageChannels
import chat.rocket.android.db.model.MessageEntity import chat.rocket.android.db.model.MessageEntity
import chat.rocket.android.db.model.MessageFavoritesRelation import chat.rocket.android.db.model.MessageFavoritesRelation
import chat.rocket.android.db.model.MessageMentionsRelation import chat.rocket.android.db.model.MessageMentionsRelation
import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.db.model.ReactionEntity import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.db.model.UserStatus import chat.rocket.android.db.model.UserStatus
import chat.rocket.android.db.model.asEntity import chat.rocket.android.db.model.asEntity
import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.extensions.removeTrailingSlash import chat.rocket.android.util.extensions.removeTrailingSlash
import chat.rocket.android.util.extensions.toEntity import chat.rocket.android.util.extensions.toEntity
import chat.rocket.android.util.extensions.userId import chat.rocket.android.util.extensions.userId
...@@ -31,6 +33,7 @@ import chat.rocket.core.model.Room ...@@ -31,6 +33,7 @@ import chat.rocket.core.model.Room
import chat.rocket.core.model.attachment.Attachment import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.userId import chat.rocket.core.model.userId
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
...@@ -38,14 +41,17 @@ import timber.log.Timber ...@@ -38,14 +41,17 @@ import timber.log.Timber
import java.util.HashSet import java.util.HashSet
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class DatabaseManager(val context: Application, class DatabaseManager(val context: Application, val serverUrl: String) {
val serverUrl: String) {
private val database: RCDatabase = androidx.room.Room.databaseBuilder(context, private val database: RCDatabase = androidx.room.Room.databaseBuilder(context,
RCDatabase::class.java, serverUrl.databaseName()) RCDatabase::class.java, serverUrl.databaseName())
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
val dbContext = newSingleThreadContext("$serverUrl-db-context") private val dbContext = newSingleThreadContext("$serverUrl-db-context")
private val dbManagerContext = newSingleThreadContext("$serverUrl-db-manager-context")
private val writeChannel = Channel<Operation>(Channel.UNLIMITED)
private var dbJob: Job? = null
private val insertSubs = HashMap<String, Subscription>() private val insertSubs = HashMap<String, Subscription>()
private val insertRooms = HashMap<String, Room>() private val insertRooms = HashMap<String, Room>()
...@@ -56,9 +62,32 @@ class DatabaseManager(val context: Application, ...@@ -56,9 +62,32 @@ class DatabaseManager(val context: Application,
fun userDao(): UserDao = database.userDao() fun userDao(): UserDao = database.userDao()
fun messageDao(): MessageDao = database.messageDao() fun messageDao(): MessageDao = database.messageDao()
init {
start()
}
fun start() {
dbJob?.cancel()
dbJob = launch(dbContext) {
for (operation in writeChannel) {
doOperation(operation)
}
}
}
fun stop() {
dbJob?.cancel()
dbJob = null
}
suspend fun sendOperation(operation: Operation) {
Timber.d("writerChannel: $writeChannel, closedForSend: ${writeChannel.isClosedForSend}, closedForReceive: ${writeChannel.isClosedForReceive}, empty: ${writeChannel.isEmpty}, full: ${writeChannel.isFull}")
writeChannel.send(operation)
}
suspend fun clearUsersStatus() { suspend fun clearUsersStatus() {
withContext(dbContext) { withContext(dbManagerContext) {
userDao().clearStatus() sendOperation(Operation.ClearStatus)
} }
} }
...@@ -66,15 +95,14 @@ class DatabaseManager(val context: Application, ...@@ -66,15 +95,14 @@ class DatabaseManager(val context: Application,
database.clearAllTables() database.clearAllTables()
} }
suspend fun getRoom(id: String) = withContext(dbContext) { suspend fun getRoom(id: String) = withContext(dbManagerContext) {
chatRoomDao().get(id) chatRoomDao().get(id)
} }
fun processUsersBatch(users: List<User>) { fun processUsersBatch(users: List<User>) {
launch(dbContext) { launch(dbManagerContext) {
val dao = userDao()
val list = ArrayList<BaseUserEntity>(users.size) val list = ArrayList<BaseUserEntity>(users.size)
var time = measureTimeMillis { val time = measureTimeMillis {
users.forEach { user -> users.forEach { user ->
user.toEntity()?.let { entity -> user.toEntity()?.let { entity ->
list.add(entity) list.add(entity)
...@@ -82,9 +110,7 @@ class DatabaseManager(val context: Application, ...@@ -82,9 +110,7 @@ class DatabaseManager(val context: Application,
} }
} }
Timber.d("Converted users batch(${users.size}) in $time MS") Timber.d("Converted users batch(${users.size}) in $time MS")
sendOperation(Operation.InsertUsers(list))
time = measureTimeMillis { dao.upsert(list) }
Timber.d("Upserted users batch(${users.size}) in $time MS")
} }
} }
...@@ -92,16 +118,16 @@ class DatabaseManager(val context: Application, ...@@ -92,16 +118,16 @@ class DatabaseManager(val context: Application,
* Creates a list of data base operations * Creates a list of data base operations
*/ */
fun processChatRoomsBatch(batch: List<StreamMessage<BaseRoom>>) { fun processChatRoomsBatch(batch: List<StreamMessage<BaseRoom>>) {
launch(dbContext) { launch(dbManagerContext) {
val toRemove = HashSet<String>() val toRemove = HashSet<String>()
val toInsert = ArrayList<ChatRoomEntity>(batch.size / 2) val toInsert = ArrayList<ChatRoomEntity>(batch.size / 2)
val toUpdate = ArrayList<ChatRoomEntity>(batch.size) val toUpdate = ArrayList<ChatRoomEntity>(batch.size)
batch.forEach { batch.forEach {
when(it.type) { when (it.type) {
is Type.Removed -> toRemove.add(removeChatRoom(it.data)) is Type.Removed -> toRemove.add(removeChatRoom(it.data))
is Type.Inserted -> insertChatRoom(it.data)?.let { toInsert.add(it) } is Type.Inserted -> insertChatRoom(it.data)?.let { toInsert.add(it) }
is Type.Updated -> { is Type.Updated -> {
when(it.data) { when (it.data) {
is Subscription -> updateSubs[(it.data as Subscription).roomId] = it.data as Subscription is Subscription -> updateSubs[(it.data as Subscription).roomId] = it.data as Subscription
is Room -> updateRooms[(it.data as Room).id] = it.data as Room is Room -> updateRooms[(it.data as Room).id] = it.data as Room
} }
...@@ -116,65 +142,51 @@ class DatabaseManager(val context: Application, ...@@ -116,65 +142,51 @@ class DatabaseManager(val context: Application,
val filteredUpdate = toUpdate.filterNot { toRemove.contains(it.id) } val filteredUpdate = toUpdate.filterNot { toRemove.contains(it.id) }
val filteredInsert = toInsert.filterNot { toRemove.contains(it.id) } val filteredInsert = toInsert.filterNot { toRemove.contains(it.id) }
Timber.d("Running ChatRooms transaction: remove: $toRemove - insert: $toInsert - update: $filteredUpdate") sendOperation(Operation.UpdateRooms(filteredInsert, filteredUpdate, toRemove.toList()))
chatRoomDao().update(filteredInsert, filteredUpdate, toRemove.toList())
//updateMessages(batch)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error updating chatrooms") Timber.d(ex, "Error updating chatrooms")
} }
} }
} }
private fun updateMessages(batch: List<StreamMessage<BaseRoom>>) {
val list = batch.filterNot { it.type == Type.Removed }
.filter { it.data is Room }
.filterNot { (it.data as Room).lastMessage == null }
.map { (it.data as Room).lastMessage!! }
processMessagesBatch(list)
}
fun updateSelfUser(myself: Myself) { fun updateSelfUser(myself: Myself) {
launch(dbContext) { launch(dbManagerContext) {
val user = userDao().getUser(myself.id) val user = userDao().getUser(myself.id)
val entity = user?.copy( val entity = user?.copy(
name = myself.name ?: user.name, name = myself.name ?: user.name,
username = myself.username ?: user.username, username = myself.username ?: user.username,
utcOffset = myself.utcOffset ?: user.utcOffset, utcOffset = myself.utcOffset ?: user.utcOffset,
status = myself.status?.toString() ?: user.status status = myself.status?.toString() ?: user.status
) ?: myself.asUser().toEntity() ) ?: myself.asUser().toEntity()
Timber.d("UPDATING SELF: $entity") Timber.d("UPDATING SELF: $entity")
entity?.let { userDao().upsert(entity) } entity?.let { sendOperation(Operation.UpsertUser(it)) }
} }
} }
fun processRooms(rooms: List<ChatRoom>) { fun processRooms(rooms: List<ChatRoom>) {
launch(dbContext) { launch(dbManagerContext) {
val entities = rooms.map { mapChatRoom(it) } val entities = rooms.map { mapChatRoom(it) }
chatRoomDao().insertOrReplace(entities) sendOperation(Operation.InsertRooms(entities))
} }
} }
fun processMessagesBatch(messages: List<Message>): Job { fun processMessagesBatch(messages: List<Message>): Job {
return launch(dbContext) { return launch(dbManagerContext) {
val dao = messageDao()
val list = mutableListOf<Pair<MessageEntity, List<BaseMessageEntity>>>() val list = mutableListOf<Pair<MessageEntity, List<BaseMessageEntity>>>()
messages.forEach { message -> messages.forEach { message ->
val pair = createMessageEntities(message) val pair = createMessageEntities(message)
list.add(pair) list.add(pair)
} }
dao.insert(list) sendOperation(Operation.InsertMessages(list))
} }
} }
private suspend fun createMessageEntities(message: Message): Pair<MessageEntity, List<BaseMessageEntity>> { private suspend fun createMessageEntities(message: Message): Pair<MessageEntity, List<BaseMessageEntity>> {
val messageEntity = message.toEntity() val messageEntity = message.toEntity()
val list = mutableListOf<BaseMessageEntity>() val list = mutableListOf<BaseMessageEntity>()
createAttachments(message)?.let { list.addAll(it) } createAttachments(message)?.let { list.addAll(it) }
createFavoriteRelations(message)?.let { list.addAll(it) } createFavoriteRelations(message)?.let { list.addAll(it) }
createMentionRelations(message)?.let { list.addAll(it) } createMentionRelations(message)?.let { list.addAll(it) }
createChannelRelations(message)?.let { list.addAll(it) } createChannelRelations(message)?.let { list.addAll(it) }
...@@ -212,7 +224,7 @@ class DatabaseManager(val context: Application, ...@@ -212,7 +224,7 @@ class DatabaseManager(val context: Application,
val list = mutableListOf<UrlEntity>() val list = mutableListOf<UrlEntity>()
message.urls!!.forEach { url -> message.urls!!.forEach { url ->
list.add(UrlEntity(message.id, url.url, url.parsedUrl?.host, url.meta?.title, list.add(UrlEntity(message.id, url.url, url.parsedUrl?.host, url.meta?.title,
url.meta?.description, url.meta?.imageUrl)) url.meta?.description, url.meta?.imageUrl))
} }
return list return list
...@@ -267,7 +279,7 @@ class DatabaseManager(val context: Application, ...@@ -267,7 +279,7 @@ class DatabaseManager(val context: Application,
val list = ArrayList<BaseMessageEntity>(message.attachments!!.size) val list = ArrayList<BaseMessageEntity>(message.attachments!!.size)
message.attachments!!.forEach { attachment -> message.attachments!!.forEach { attachment ->
list.addAll(attachment.asEntity(message.id)) list.addAll(attachment.asEntity(message.id, context))
} }
return list return list
...@@ -316,7 +328,7 @@ class DatabaseManager(val context: Application, ...@@ -316,7 +328,7 @@ class DatabaseManager(val context: Application,
} }
private fun removeChatRoom(data: BaseRoom): String { private fun removeChatRoom(data: BaseRoom): String {
return when(data) { return when (data) {
is Subscription -> data.roomId is Subscription -> data.roomId
else -> data.id else -> data.id
} }
...@@ -331,14 +343,15 @@ class DatabaseManager(val context: Application, ...@@ -331,14 +343,15 @@ class DatabaseManager(val context: Application,
insertUserIfMissing(user) insertUserIfMissing(user)
chatRoom.copy( chatRoom.copy(
name = name ?: chatRoom.name, name = name ?: chatRoom.name,
fullname = fullName ?: chatRoom.fullname, fullname = fullName ?: chatRoom.fullname,
ownerId = user?.id ?: chatRoom.ownerId, ownerId = user?.id ?: chatRoom.ownerId,
readonly = readonly, readonly = readonly,
updatedAt = updatedAt ?: chatRoom.updatedAt, updatedAt = updatedAt ?: chatRoom.updatedAt,
lastMessageText = mapLastMessageText(lastMessage), lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id, lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp lastMessageTimestamp = lastMessage?.timestamp,
muted = muted
) )
} }
} }
...@@ -373,30 +386,32 @@ class DatabaseManager(val context: Application, ...@@ -373,30 +386,32 @@ class DatabaseManager(val context: Application,
val chatRoom = current.chatRoom val chatRoom = current.chatRoom
chatRoom.copy( chatRoom.copy(
id = roomId, id = roomId,
subscriptionId = id, subscriptionId = id,
type = type.toString(), type = type.toString(),
name = name ?: throw NullPointerException(), // this should be filtered on the SDK name = name
fullname = fullName ?: chatRoom.fullname, ?: throw NullPointerException(), // this should be filtered on the SDK
userId = userId ?: chatRoom.userId, fullname = fullName ?: chatRoom.fullname,
readonly = readonly ?: chatRoom.readonly, userId = userId ?: chatRoom.userId,
isDefault = isDefault, readonly = readonly ?: chatRoom.readonly,
favorite = isFavorite, isDefault = isDefault,
open = open, favorite = isFavorite,
alert = alert, open = open,
unread = unread, alert = alert,
userMentions = userMentions ?: chatRoom.userMentions, unread = unread,
groupMentions = groupMentions ?: chatRoom.groupMentions, userMentions = userMentions ?: chatRoom.userMentions,
updatedAt = updatedAt ?: chatRoom.updatedAt, groupMentions = groupMentions ?: chatRoom.groupMentions,
timestamp = timestamp ?: chatRoom.timestamp, updatedAt = updatedAt ?: chatRoom.updatedAt,
lastSeen = lastSeen ?: chatRoom.lastSeen timestamp = timestamp ?: chatRoom.timestamp,
lastSeen = lastSeen ?: chatRoom.lastSeen,
muted = chatRoom.muted
) )
} }
} }
} }
private suspend fun insertChatRoom(data: BaseRoom): ChatRoomEntity? { private suspend fun insertChatRoom(data: BaseRoom): ChatRoomEntity? {
return when(data) { return when (data) {
is Room -> insertRoom(data) is Room -> insertRoom(data)
is Subscription -> insertSubscription(data) is Subscription -> insertSubscription(data)
else -> null else -> null
...@@ -438,7 +453,8 @@ class DatabaseManager(val context: Application, ...@@ -438,7 +453,8 @@ class DatabaseManager(val context: Application,
id = room.id, id = room.id,
subscriptionId = subscription.id, subscriptionId = subscription.id,
type = room.type.toString(), type = room.type.toString(),
name = room.name ?: subscription.name ?: throw NullPointerException(), // this should be filtered on the SDK name = room.name ?: subscription.name
?: throw NullPointerException(), // this should be filtered on the SDK
fullname = subscription.fullName ?: room.fullName, fullname = subscription.fullName ?: room.fullName,
userId = userId, userId = userId,
ownerId = room.user?.id, ownerId = room.user?.id,
...@@ -493,38 +509,91 @@ class DatabaseManager(val context: Application, ...@@ -493,38 +509,91 @@ class DatabaseManager(val context: Application,
lastMessageText = mapLastMessageText(lastMessage), lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id, lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp, lastMessageTimestamp = lastMessage?.timestamp,
broadcast = broadcast broadcast = broadcast,
muted = room.muted
) )
} }
} }
suspend fun insert(rooms: List<ChatRoomEntity>) { suspend fun insert(rooms: List<ChatRoomEntity>) {
withContext(dbContext) { withContext(dbManagerContext) {
chatRoomDao().cleanInsert(rooms) sendOperation(Operation.CleanInsertRooms(rooms))
} }
} }
suspend fun insert(user: UserEntity) { suspend fun insert(user: UserEntity) {
withContext(dbContext) { sendOperation(Operation.InsertUser(user))
userDao().insert(user)
}
} }
private suspend fun insertUserIfMissing(id: String?) { private suspend fun insertUserIfMissing(id: String?) {
if (id != null && findUser(id) == null) { if (id != null && findUser(id) == null) {
Timber.d("Missing user, inserting: $id") Timber.d("Missing user, inserting: $id")
insert(UserEntity(id)) sendOperation(Operation.InsertUser(UserEntity(id)))
} }
} }
private suspend fun insertUserIfMissing(user: SimpleUser?) { private suspend fun insertUserIfMissing(user: SimpleUser?) {
if (user?.id != null && findUser(user.id!!) == null) { if (user?.id != null && findUser(user.id!!) == null) {
Timber.d("Missing user, inserting: ${user.id}") Timber.d("Missing user, inserting: ${user.id}")
insert(UserEntity(user.id!!, user.username, user.name)) sendOperation(Operation.InsertUser(UserEntity(user.id!!, user.username, user.name)))
} }
} }
fun findUser(userId: String): String? = userDao().findUser(userId) private fun findUser(userId: String): String? = userDao().findUser(userId)
private fun doOperation(operation: Operation) {
when (operation) {
is Operation.ClearStatus -> userDao().clearStatus()
is Operation.UpdateRooms -> {
Timber.d("Running ChatRooms transaction: remove: ${operation.toRemove} - insert: ${operation.toInsert} - update: ${operation.toUpdate}")
chatRoomDao().update(operation.toInsert, operation.toUpdate, operation.toRemove)
}
is Operation.InsertRooms -> {
chatRoomDao().insertOrReplace(operation.chatRooms)
}
is Operation.CleanInsertRooms -> {
chatRoomDao().cleanInsert(operation.chatRooms)
}
is Operation.InsertUsers -> {
val time = measureTimeMillis { userDao().upsert(operation.users) }
Timber.d("Upserted users batch(${operation.users.size}) in $time MS")
}
is Operation.InsertUser -> {
userDao().insert(operation.user)
}
is Operation.UpsertUser -> {
userDao().upsert(operation.user)
}
is Operation.InsertMessages -> {
messageDao().insert(operation.list)
}
is Operation.SaveLastSync -> {
messageDao().saveLastSync(operation.sync)
}
}.exhaustive
}
}
sealed class Operation {
object ClearStatus : Operation()
data class UpdateRooms(
val toInsert: List<ChatRoomEntity>,
val toUpdate: List<ChatRoomEntity>,
val toRemove: List<String>
) : Operation()
data class InsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
data class CleanInsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
data class InsertUsers(val users: List<BaseUserEntity>) : Operation()
data class UpsertUser(val user: BaseUserEntity) : Operation()
data class InsertUser(val user: UserEntity) : Operation()
data class InsertMessages(val list: List<Pair<MessageEntity, List<BaseMessageEntity>>>) : Operation()
data class SaveLastSync(val sync: MessagesSync) : Operation()
} }
fun User.toEntity(): BaseUserEntity? { fun User.toEntity(): BaseUserEntity? {
...@@ -543,10 +612,10 @@ private fun Myself.asUser(): User { ...@@ -543,10 +612,10 @@ private fun Myself.asUser(): User {
private fun String.databaseName(): String { private fun String.databaseName(): String {
val tmp = this.removePrefix("https://") val tmp = this.removePrefix("https://")
.removePrefix("http://") .removePrefix("http://")
.removeTrailingSlash() .removeTrailingSlash()
.replace("/","-") .replace("/", "-")
.replace(".", "_") .replace(".", "_")
return "$tmp.db" return "$tmp.db"
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.db ...@@ -2,6 +2,7 @@ package chat.rocket.android.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import chat.rocket.android.db.model.AttachmentActionEntity import chat.rocket.android.db.model.AttachmentActionEntity
import chat.rocket.android.db.model.AttachmentEntity import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.AttachmentFieldEntity import chat.rocket.android.db.model.AttachmentFieldEntity
...@@ -14,6 +15,7 @@ import chat.rocket.android.db.model.MessagesSync ...@@ -14,6 +15,7 @@ import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.db.model.ReactionEntity import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.emoji.internal.db.StringListConverter
@Database( @Database(
entities = [ entities = [
...@@ -23,11 +25,12 @@ import chat.rocket.android.db.model.UserEntity ...@@ -23,11 +25,12 @@ import chat.rocket.android.db.model.UserEntity
AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class, AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class,
ReactionEntity::class, MessagesSync::class ReactionEntity::class, MessagesSync::class
], ],
version = 9, version = 12,
exportSchema = true exportSchema = true
) )
@TypeConverters(StringListConverter::class)
abstract class RCDatabase : RoomDatabase() { abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao abstract fun chatRoomDao(): ChatRoomDao
abstract fun messageDao(): MessageDao abstract fun messageDao(): MessageDao
} }
\ No newline at end of file
package chat.rocket.android.db.model package chat.rocket.android.db.model
import android.content.Context
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import chat.rocket.android.R
import chat.rocket.android.util.extension.orFalse import chat.rocket.android.util.extension.orFalse
import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.core.model.attachment.Attachment import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.attachment.actions.ButtonAction import chat.rocket.core.model.attachment.actions.ButtonAction
import timber.log.Timber
@Entity(tableName = "attachments", @Entity(tableName = "attachments",
foreignKeys = [ foreignKeys = [
...@@ -115,150 +109,54 @@ data class AttachmentActionEntity( ...@@ -115,150 +109,54 @@ data class AttachmentActionEntity(
var id: Long? = null var id: Long? = null
} }
fun Attachment.asEntity(msgId: String): List<BaseMessageEntity> { fun Attachment.asEntity(msgId: String, context: Context): List<BaseMessageEntity> {
return when(this) { val attachmentId = "${msgId}_${hashCode()}"
is ImageAttachment -> listOf(asEntity(msgId))
is VideoAttachment -> listOf(asEntity(msgId))
is AudioAttachment -> listOf(asEntity(msgId))
is AuthorAttachment -> asEntity(msgId)
is ColorAttachment -> asEntity(msgId)
is MessageAttachment -> listOf(asEntity(msgId))
is GenericFileAttachment -> listOf(asEntity(msgId))
is ActionsAttachment -> asEntity(msgId)
else -> {
Timber.d("Missing conversion for: ${javaClass.canonicalName}")
emptyList()
}
}
}
fun ImageAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
imageUrl = url,
imageType = type,
imageSize = size
)
fun VideoAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
videoUrl = url,
videoType = type,
videoSize = size
)
fun AudioAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
audioUrl = url,
audioType = type,
audioSize = size
)
fun AuthorAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
val list = mutableListOf<BaseMessageEntity>() val list = mutableListOf<BaseMessageEntity>()
val attachment = AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
authorLink = url,
authorIcon = authorIcon,
authorName = authorName,
hasFields = fields?.isNotEmpty() == true
)
list.add(attachment)
fields?.forEach { field -> val text = mapAttachmentText(text, attachments?.firstOrNull(), context)
val entity = AttachmentFieldEntity(
attachmentId = attachment._id,
title = field.title,
value = field.value
)
list.add(entity)
}
return list val entity = AttachmentEntity(
} _id = attachmentId,
messageId = msgId,
fun ColorAttachment.asEntity(msgId: String): List<BaseMessageEntity> { title = title,
val list = mutableListOf<BaseMessageEntity>() type = type,
val attachment = AttachmentEntity( description = description,
_id = "${msgId}_${hashCode()}", text = text,
messageId = msgId, titleLink = titleLink,
color = color.rawColor, titleLinkDownload = titleLinkDownload.orFalse(),
fallback = fallback, imageUrl = imageUrl,
hasFields = fields?.isNotEmpty() == true imageType = imageType,
imageSize = imageSize,
videoUrl = videoUrl,
videoType = videoType,
videoSize = videoSize,
audioUrl = audioUrl,
audioType = audioType,
audioSize = audioSize,
authorLink = authorLink,
authorIcon = authorIcon,
authorName = authorName,
color = color?.rawColor,
fallback = fallback,
thumbUrl = thumbUrl,
messageLink = messageLink,
timestamp = timestamp,
buttonAlignment = buttonAlignment,
hasActions = actions?.isNotEmpty() == true,
hasFields = fields?.isNotEmpty() == true
) )
list.add(attachment) list.add(entity)
fields?.forEach { field -> fields?.forEach { field ->
val entity = AttachmentFieldEntity( val entity = AttachmentFieldEntity(
attachmentId = attachment._id, attachmentId = attachmentId,
title = field.title, title = field.title,
value = field.value value = field.value
) )
list.add(entity) list.add(entity)
} }
return list actions?.forEach { action ->
}
// TODO - how to model An message attachment with attachments???
fun MessageAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
authorName = author,
authorIcon = icon,
text = text,
thumbUrl = thumbUrl,
color = color?.rawColor,
messageLink = url,
timestamp = timestamp
)
fun GenericFileAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload ?: false
)
fun ActionsAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
val list = mutableListOf<BaseMessageEntity>()
val attachmentId = "${msgId}_${hashCode()}"
val attachment = AttachmentEntity(
_id = attachmentId,
messageId = msgId,
title = title,
hasActions = true,
buttonAlignment = buttonAlignment
)
list.add(attachment)
actions.forEach { action ->
when (action) { when (action) {
is ButtonAction -> AttachmentActionEntity( is ButtonAction -> AttachmentActionEntity(
attachmentId = attachmentId, attachmentId = attachmentId,
...@@ -275,4 +173,20 @@ fun ActionsAttachment.asEntity(msgId: String): List<BaseMessageEntity> { ...@@ -275,4 +173,20 @@ fun ActionsAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
}?.let { list.add(it) } }?.let { list.add(it) }
} }
return list return list
} }
\ No newline at end of file
fun mapAttachmentText(text: String?, attachment: Attachment?, context: Context): String? {
return if (attachment != null) {
when {
attachment.imageUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_photo)
attachment.videoUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_video)
attachment.audioUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_audio)
attachment.titleLink.isNotNullNorEmpty() &&
attachment.type?.contentEquals("file") == true ->
context.getString(R.string.msg_preview_file)
else -> text
}
} else {
text
}
}
...@@ -5,6 +5,8 @@ import androidx.room.Entity ...@@ -5,6 +5,8 @@ import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import chat.rocket.android.emoji.internal.db.StringListConverter
@Entity(tableName = "chatrooms", @Entity(tableName = "chatrooms",
indices = [ indices = [
...@@ -20,6 +22,7 @@ import androidx.room.PrimaryKey ...@@ -20,6 +22,7 @@ import androidx.room.PrimaryKey
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"]) ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"])
] ]
) )
@TypeConverters(StringListConverter::class)
data class ChatRoomEntity( data class ChatRoomEntity(
@PrimaryKey var id: String, @PrimaryKey var id: String,
var subscriptionId: String, var subscriptionId: String,
...@@ -42,7 +45,8 @@ data class ChatRoomEntity( ...@@ -42,7 +45,8 @@ data class ChatRoomEntity(
var lastMessageText: String? = null, var lastMessageText: String? = null,
var lastMessageUserId: String? = null, var lastMessageUserId: String? = null,
var lastMessageTimestamp: Long? = null, var lastMessageTimestamp: Long? = null,
var broadcast: Boolean? = false var broadcast: Boolean? = false,
var muted: List<String>? = null
) )
data class ChatRoom( data class ChatRoom(
...@@ -52,4 +56,4 @@ data class ChatRoom( ...@@ -52,4 +56,4 @@ data class ChatRoom(
var status: String?, var status: String?,
var lastMessageUserName: String?, var lastMessageUserName: String?,
var lastMessageUserFullName: String? var lastMessageUserFullName: String?
) )
\ No newline at end of file
...@@ -35,7 +35,7 @@ object ImageHelper { ...@@ -35,7 +35,7 @@ object ImageHelper {
// TODO - implement a proper image viewer with a proper Transition // TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer // TODO - We should definitely write our own ImageViewer
fun openImage(context: Context, imageUrl: String, imageName: String) { fun openImage(context: Context, imageUrl: String, imageName: String? = "") {
var imageViewer: ImageViewer? = null var imageViewer: ImageViewer? = null
val request = val request =
ImageRequestBuilder.newBuilderWithSource(imageUrl.toUri()) ImageRequestBuilder.newBuilderWithSource(imageUrl.toUri())
......
...@@ -17,7 +17,7 @@ class MessageHelper @Inject constructor( ...@@ -17,7 +17,7 @@ class MessageHelper @Inject constructor(
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val settings: PublicSettings = getSettingsInteractor.get(currentServer) private val settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun createPermalink(message: Message, chatRoom: ChatRoom): String { fun createPermalink(message: Message, chatRoom: ChatRoom, markdownSyntax: Boolean = true): String {
val type = when (chatRoom.type) { val type = when (chatRoom.type) {
is RoomType.PrivateGroup -> "group" is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel" is RoomType.Channel -> "channel"
...@@ -30,7 +30,8 @@ class MessageHelper @Inject constructor( ...@@ -30,7 +30,8 @@ class MessageHelper @Inject constructor(
} else { } else {
chatRoom.name chatRoom.name
} }
return "[ ]($currentServer/$type/$name?msg=${message.id}) " val permalink = "$currentServer/$type/$name?msg=${message.id}"
return if (markdownSyntax) "[ ]($permalink) " else permalink
} }
fun messageIdFromPermalink(permalink: String): String? { fun messageIdFromPermalink(permalink: String): String? {
......
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import android.content.Context import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.emoji.Emoji import chat.rocket.android.emoji.Emoji
...@@ -36,13 +35,9 @@ import chat.rocket.common.model.UserStatus ...@@ -36,13 +35,9 @@ import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getCustomEmojis import chat.rocket.core.internal.rest.getCustomEmojis
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -51,23 +46,33 @@ class MainPresenter @Inject constructor( ...@@ -51,23 +46,33 @@ class MainPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val refreshPermissionsInteractor: RefreshPermissionsInteractor, private val refreshPermissionsInteractor: RefreshPermissionsInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderUiModelMapper, private val navHeaderMapper: NavHeaderUiModelMapper,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor,
factory: RocketChatClientFactory,
private val groupedPush: GroupedPush, private val groupedPush: GroupedPush,
serverInteractor: GetCurrentServerInteractor,
localRepository: LocalRepository,
removeAccountInteractor: RemoveAccountInteractor,
factory: RocketChatClientFactory,
dbManagerFactory: DatabaseManagerFactory, dbManagerFactory: DatabaseManagerFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, factory, view = view) { ) : CheckServerPresenter(
strategy = strategy,
factory = factory,
serverInteractor = serverInteractor,
localRepository = localRepository,
removeAccountInteractor = removeAccountInteractor,
tokenRepository = tokenRepository,
managerFactory = managerFactory,
dbManagerFactory = dbManagerFactory,
tokenView = view,
navigator = navigator
) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer) private val manager = managerFactory.create(currentServer)
private val dbManager = dbManagerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val userDataChannel = Channel<Myself>() private val userDataChannel = Channel<Myself>()
...@@ -114,9 +119,7 @@ class MainPresenter @Inject constructor( ...@@ -114,9 +119,7 @@ class MainPresenter @Inject constructor(
view.setupUserAccountInfo(model) view.setupUserAccountInfo(model)
} catch (ex: Exception) { } catch (ex: Exception) {
when (ex) { when (ex) {
is RocketChatAuthException -> { is RocketChatAuthException -> logout()
logout()
}
else -> { else -> {
Timber.d(ex, "Error loading my information for navheader") Timber.d(ex, "Error loading my information for navheader")
ex.message?.let { ex.message?.let {
...@@ -163,36 +166,9 @@ class MainPresenter @Inject constructor( ...@@ -163,36 +166,9 @@ class MainPresenter @Inject constructor(
} }
} }
/**
* Logout from current server.
*/
fun logout() { fun logout() {
launchUI(strategy) { setupConnectionInfo(currentServer)
view.showProgress() super.logout(userDataChannel)
try {
clearTokens()
retryIO("logout") { client.logout() }
} catch (exception: RocketChatException) {
Timber.d(exception, "Error calling logout")
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
try {
disconnect()
removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
navigator.switchOrAddNewServer()
} catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...")
}
view.hideProgress()
}
} }
fun connect() { fun connect() {
...@@ -202,8 +178,8 @@ class MainPresenter @Inject constructor( ...@@ -202,8 +178,8 @@ class MainPresenter @Inject constructor(
} }
fun disconnect() { fun disconnect() {
manager.removeUserDataChannel(userDataChannel) setupConnectionInfo(currentServer)
manager.disconnect() super.disconnect(userDataChannel)
} }
fun changeServer(serverUrl: String) { fun changeServer(serverUrl: String) {
...@@ -247,20 +223,6 @@ class MainPresenter @Inject constructor( ...@@ -247,20 +223,6 @@ class MainPresenter @Inject constructor(
saveAccountInteractor.save(account) saveAccountInteractor.save(account)
} }
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
view.invalidateToken(pushToken)
} catch (ex: Exception) {
Timber.d(ex, "Error unregistering push token")
}
}
localRepository.clearAllFromServer(currentServer)
}
private suspend fun subscribeMyselfUpdates() { private suspend fun subscribeMyselfUpdates() {
manager.addUserDataChannel(userDataChannel) manager.addUserDataChannel(userDataChannel)
for (myself in userDataChannel) { for (myself in userDataChannel) {
......
...@@ -4,9 +4,10 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView ...@@ -4,9 +4,10 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.uimodel.NavHeaderUiModel import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.presentation.TokenView
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
interface MainView : MessageView, VersionCheckView { interface MainView : MessageView, VersionCheckView, TokenView {
/** /**
* Shows the current user status. * Shows the current user status.
...@@ -31,8 +32,6 @@ interface MainView : MessageView, VersionCheckView { ...@@ -31,8 +32,6 @@ interface MainView : MessageView, VersionCheckView {
fun closeServerSelection() fun closeServerSelection()
fun invalidateToken(token: String)
fun showProgress() fun showProgress()
fun hideProgress() fun hideProgress()
......
...@@ -202,8 +202,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -202,8 +202,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
.show() .show()
} }
override fun invalidateToken(token: String) = override fun invalidateToken(token: String) = invalidateFirebaseToken(token)
invalidateFirebaseToken(token)
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) = showToast(resId)
...@@ -234,11 +233,11 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -234,11 +233,11 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
fun showLogoutDialog() { fun showLogoutDialog() {
val builder = AlertDialog.Builder(this) val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.action_logout) builder.setTitle(R.string.title_are_you_sure)
builder.setMessage(R.string.title_confirmation) .setPositiveButton(R.string.action_logout) { _, _ -> presenter.logout()}
builder.setPositiveButton(R.string.action_logout) { _, _ -> presenter.logout()} .setNegativeButton(android.R.string.no) { dialog, _ -> dialog.cancel() }
.setNegativeButton(R.string.action_stay) { dialog, _ -> dialog.cancel() } .create()
builder.create().show() .show()
} }
fun setAvatar(avatarUrl: String) { fun setAvatar(avatarUrl: String) {
......
...@@ -5,19 +5,31 @@ import android.net.Uri ...@@ -5,19 +5,31 @@ import android.net.Uri
import chat.rocket.android.chatroom.domain.UriInteractor import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.core.behaviours.showMessage import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.compressImageAndGetByteArray import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.gethash
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extension.toHex
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.deleteOwnAccount
import chat.rocket.core.internal.rest.resetAvatar import chat.rocket.core.internal.rest.resetAvatar
import chat.rocket.core.internal.rest.setAvatar import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile import chat.rocket.core.internal.rest.updateProfile
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import java.lang.Exception
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
...@@ -26,8 +38,23 @@ class ProfilePresenter @Inject constructor( ...@@ -26,8 +38,23 @@ class ProfilePresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val uriInteractor: UriInteractor, private val uriInteractor: UriInteractor,
val userHelper: UserHelper, val userHelper: UserHelper,
navigator: MainNavigator,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory factory: RocketChatClientFactory,
removeAccountInteractor: RemoveAccountInteractor,
tokenRepository: TokenRepository,
dbManagerFactory: DatabaseManagerFactory,
managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(
strategy = strategy,
factory = factory,
serverInteractor = serverInteractor,
removeAccountInteractor = removeAccountInteractor,
tokenRepository = tokenRepository,
dbManagerFactory = dbManagerFactory,
managerFactory = managerFactory,
tokenView = view,
navigator = navigator
) { ) {
private val serverUrl = serverInteractor.get()!! private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl) private val client: RocketChatClient = factory.create(serverUrl)
...@@ -147,4 +174,27 @@ class ProfilePresenter @Inject constructor( ...@@ -147,4 +174,27 @@ class ProfilePresenter @Inject constructor(
} }
} }
} }
fun deleteAccount(password: String) {
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
// REMARK: Backend API is only working with a lowercase hash.
// https://github.com/RocketChat/Rocket.Chat/issues/12573
retryIO { client.deleteOwnAccount(password.gethash().toHex().toLowerCase()) }
setupConnectionInfo(serverUrl)
logout(null)
}
} catch (exception: Exception) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
} }
\ No newline at end of file
...@@ -2,8 +2,9 @@ package chat.rocket.android.profile.presentation ...@@ -2,8 +2,9 @@ package chat.rocket.android.profile.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.server.presentation.TokenView
interface ProfileView : LoadingView, MessageView { interface ProfileView : TokenView, LoadingView, MessageView {
/** /**
* Shows the user profile. * Shows the user profile.
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.profile.ui ...@@ -2,6 +2,7 @@ package chat.rocket.android.profile.ui
import DrawableHelper import DrawableHelper
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
...@@ -11,6 +12,8 @@ import android.view.Menu ...@@ -11,6 +12,8 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.MenuInflater
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.net.toUri import androidx.core.net.toUri
...@@ -29,6 +32,7 @@ import chat.rocket.android.util.extensions.inflate ...@@ -29,6 +32,7 @@ import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.invalidateFirebaseToken
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
...@@ -61,6 +65,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -61,6 +65,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
} }
override fun onCreateView( override fun onCreateView(
...@@ -98,6 +103,25 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -98,6 +103,25 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
} }
} }
override fun onPrepareOptionsMenu(menu: Menu) {
if (actionMode != null) {
menu.clear()
}
super.onPrepareOptionsMenu(menu)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.profile, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_delete_account -> showDeleteAccountDialog()
}
return true
}
override fun showProfile(avatarUrl: String, name: String, username: String, email: String?) { override fun showProfile(avatarUrl: String, name: String, username: String, email: String?) {
ui { ui {
image_avatar.setImageURI(avatarUrl) image_avatar.setImageURI(avatarUrl)
...@@ -123,6 +147,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -123,6 +147,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
showMessage(getString(R.string.msg_profile_update_successfully)) showMessage(getString(R.string.msg_profile_update_successfully))
} }
override fun invalidateToken(token: String) = invalidateFirebaseToken(token)
override fun showLoading() { override fun showLoading() {
enableUserInput(false) enableUserInput(false)
ui { view_loading.isVisible = true } ui { view_loading.isVisible = true }
...@@ -148,7 +174,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -148,7 +174,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.profile, menu) mode.menuInflater.inflate(R.menu.action_mode_profile, menu)
mode.title = getString(R.string.title_update_profile) mode.title = getString(R.string.title_update_profile)
return true return true
} }
...@@ -239,6 +265,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -239,6 +265,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_username.toString() != currentUsername || text_username.toString() != currentUsername ||
text_email.toString() != currentEmail) text_email.toString() != currentEmail)
}.subscribe { isValid -> }.subscribe { isValid ->
activity?.invalidateOptionsMenu()
if (isValid) { if (isValid) {
startActionMode() startActionMode()
} else { } else {
...@@ -264,4 +291,19 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -264,4 +291,19 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.isEnabled = value text_email.isEnabled = value
} }
} }
fun showDeleteAccountDialog() {
val passwordEditText = EditText(context)
passwordEditText.hint = getString(R.string.msg_password)
val builder = AlertDialog.Builder(context)
builder.setTitle(R.string.title_are_you_sure)
.setView(passwordEditText)
.setPositiveButton(R.string.action_delete_account) { _, _ ->
presenter.deleteAccount(passwordEditText.text.toString())
}
.setNegativeButton(android.R.string.no) { dialog, _ -> dialog.cancel() }
.create()
.show()
}
} }
...@@ -12,18 +12,10 @@ import chat.rocket.common.model.SimpleUser ...@@ -12,18 +12,10 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Reactions import chat.rocket.core.model.Reactions
import chat.rocket.core.model.attachment.Attachment import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.Color import chat.rocket.core.model.attachment.Color
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.DEFAULT_COLOR_STR import chat.rocket.core.model.attachment.DEFAULT_COLOR_STR
import chat.rocket.core.model.attachment.Field import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.Action import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.attachment.actions.ButtonAction import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.messageTypeOf import chat.rocket.core.model.messageTypeOf
import chat.rocket.core.model.url.Meta import chat.rocket.core.model.url.Meta
...@@ -141,44 +133,57 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -141,44 +133,57 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val list = mutableListOf<Attachment>() val list = mutableListOf<Attachment>()
attachments.forEach { attachment -> attachments.forEach { attachment ->
with(attachment) { with(attachment) {
when { val fields = if (hasFields) {
imageUrl != null -> { withContext(CommonPool) {
ImageAttachment(title, description, text, titleLink, titleLinkDownload, imageUrl, type, imageSize) dbManager.messageDao().getAttachmentFields(attachment._id)
} }.map { Field(it.title, it.value) }
videoUrl != null -> { } else {
VideoAttachment(title, description, text, titleLink, titleLinkDownload, videoUrl, type, videoSize) null
} }
audioUrl != null -> { val actions = if (hasActions) {
AudioAttachment(title, description, text, titleLink, titleLinkDownload, audioUrl, type, audioSize) withContext(CommonPool) {
} dbManager.messageDao().getAttachmentActions(attachment._id)
titleLink != null -> { }.mapNotNull { mapAction(it) }
GenericFileAttachment(title, description, text, titleLink, titleLink, titleLinkDownload) } else {
} null
text != null && color != null && fallback != null -> { }
ColorAttachment(Color.Custom(color), text, fallback)
} val attachment = Attachment(
text != null -> { title = title,
// TODO how to model message with attachments type = type,
MessageAttachment(authorName, authorIcon, text, thumbUrl, description = description,
color?.let { Color.Custom(it) }, messageLink, null, timestamp) authorName = authorName,
} text = text,
authorLink != null -> { thumbUrl = thumbUrl,
mapAuthorAttachment(this) color = color?.let { Color.Custom(color) },
} titleLink = titleLink,
hasFields -> { titleLinkDownload = titleLinkDownload,
mapColorAttachmentWithFields(this) imageUrl = imageUrl,
} imageType = imageType,
hasActions -> { imageSize = imageSize,
mapActionAttachment(this) videoUrl = videoUrl,
} videoType = videoType,
else -> null videoSize = videoSize,
}?.let { list.add(it) } audioUrl = audioUrl,
audioType = audioType,
audioSize = audioSize,
messageLink = messageLink,
attachments = null, // HOW TO MAP THIS
timestamp = timestamp,
authorIcon = authorIcon,
authorLink = authorLink,
fields = fields,
fallback = fallback,
buttonAlignment = if (actions != null && actions.isNotEmpty()) buttonAlignment ?: "vertical" else null,
actions = actions
)
list.add(attachment)
} }
} }
return list return list
} }
private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment { /*private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment {
val fields = withContext(CommonPool) { val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(entity._id) dbManager.messageDao().getAttachmentFields(entity._id)
}.map { Field(it.title, it.value) } }.map { Field(it.title, it.value) }
...@@ -199,7 +204,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -199,7 +204,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
// TODO - remove the default "vertical" value from here... // TODO - remove the default "vertical" value from here...
ActionsAttachment(title, actions, buttonAlignment ?: "vertical") ActionsAttachment(title, actions, buttonAlignment ?: "vertical")
} }
} }*/
private fun mapAction(action: AttachmentActionEntity): Action? { private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) { return when (action.type) {
...@@ -210,12 +215,12 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -210,12 +215,12 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
} }
} }
private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment { /*private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment {
val fields = withContext(CommonPool) { val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id) dbManager.messageDao().getAttachmentFields(attachment._id)
}.map { Field(it.title, it.value) } }.map { Field(it.title, it.value) }
return with(attachment) { return with(attachment) {
AuthorAttachment(authorLink!!, authorIcon, authorName, fields) AuthorAttachment(authorLink!!, authorIcon, authorName, fields)
} }
} }*/
} }
\ No newline at end of file
package chat.rocket.android.server.infraestructure package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.Operation
import chat.rocket.android.db.model.MessagesSync import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.server.domain.MessagesRepository import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -61,9 +62,7 @@ class DatabaseMessagesRepository( ...@@ -61,9 +62,7 @@ class DatabaseMessagesRepository(
} }
override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) { override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) {
withContext(dbManager.dbContext) { dbManager.sendOperation(Operation.SaveLastSync(MessagesSync(roomId, timeMillis)))
dbManager.messageDao().saveLastSync(MessagesSync(roomId, timeMillis))
}
} }
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) { override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) {
......
...@@ -3,7 +3,11 @@ package chat.rocket.android.server.presentation ...@@ -3,7 +3,11 @@ package chat.rocket.android.server.presentation
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.server.presentation.VersionCheckView import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.helper.OauthHelper import chat.rocket.android.helper.OauthHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.GetSettingsInteractor import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.casLoginUrl import chat.rocket.android.server.domain.casLoginUrl
...@@ -18,6 +22,12 @@ import chat.rocket.android.server.domain.isLoginFormEnabled ...@@ -18,6 +22,12 @@ import chat.rocket.android.server.domain.isLoginFormEnabled
import chat.rocket.android.server.domain.isRegistrationEnabledForNewUsers import chat.rocket.android.server.domain.isRegistrationEnabledForNewUsers
import chat.rocket.android.server.domain.isWordpressAuthenticationEnabled import chat.rocket.android.server.domain.isWordpressAuthenticationEnabled
import chat.rocket.android.server.domain.wordpressUrl import chat.rocket.android.server.domain.wordpressUrl
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.VersionInfo import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
...@@ -30,9 +40,15 @@ import chat.rocket.common.RocketChatException ...@@ -30,9 +40,15 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatInvalidProtocolException import chat.rocket.common.RocketChatInvalidProtocolException
import chat.rocket.common.model.ServerInfo import chat.rocket.common.model.ServerInfo
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.serverInfo import chat.rocket.core.internal.rest.serverInfo
import chat.rocket.core.internal.rest.settingsOauth import chat.rocket.core.internal.rest.settingsOauth
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
private const val SERVICE_NAME_FACEBOOK = "facebook" private const val SERVICE_NAME_FACEBOOK = "facebook"
...@@ -42,16 +58,26 @@ private const val SERVICE_NAME_LINKEDIN = "linkedin" ...@@ -42,16 +58,26 @@ private const val SERVICE_NAME_LINKEDIN = "linkedin"
private const val SERVICE_NAME_GILAB = "gitlab" private const val SERVICE_NAME_GILAB = "gitlab"
private const val SERVICE_NAME_WORDPRESS = "wordpress" private const val SERVICE_NAME_WORDPRESS = "wordpress"
abstract class CheckServerPresenter constructor( abstract class CheckServerPresenter constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val settingsInteractor: GetSettingsInteractor? = null, private val settingsInteractor: GetSettingsInteractor? = null,
private val view: VersionCheckView? = null private val serverInteractor: GetCurrentServerInteractor? = null,
private val localRepository: LocalRepository? = null,
private val removeAccountInteractor: RemoveAccountInteractor? = null,
private val tokenRepository: TokenRepository? = null,
private val managerFactory: ConnectionManagerFactory? = null,
private val dbManagerFactory: DatabaseManagerFactory? = null,
private val versionCheckView: VersionCheckView? = null,
private val tokenView: TokenView? = null,
private val navigator: MainNavigator? = null,
private val refreshSettingsInteractor: RefreshSettingsInteractor? = null
) { ) {
private lateinit var currentServer: String private lateinit var currentServer: String
private lateinit var client: RocketChatClient private lateinit var client: RocketChatClient
private lateinit var settings: PublicSettings private lateinit var settings: PublicSettings
private lateinit var manager: ConnectionManager
private lateinit var dbManager: DatabaseManager
internal var state: String = "" internal var state: String = ""
internal var facebookOauthUrl: String? = null internal var facebookOauthUrl: String? = null
internal var githubOauthUrl: String? = null internal var githubOauthUrl: String? = null
...@@ -61,6 +87,9 @@ abstract class CheckServerPresenter constructor( ...@@ -61,6 +87,9 @@ abstract class CheckServerPresenter constructor(
internal var wordpressOauthUrl: String? = null internal var wordpressOauthUrl: String? = null
internal var casLoginUrl: String? = null internal var casLoginUrl: String? = null
internal var casToken: String? = null internal var casToken: String? = null
internal var casServiceName: String? = null
internal var casServiceNameTextColor: Int = 0
internal var casServiceButtonColor: Int = 0
internal var customOauthUrl: String? = null internal var customOauthUrl: String? = null
internal var customOauthServiceName: String? = null internal var customOauthServiceName: String? = null
internal var customOauthServiceNameTextColor: Int = 0 internal var customOauthServiceNameTextColor: Int = 0
...@@ -75,10 +104,47 @@ abstract class CheckServerPresenter constructor( ...@@ -75,10 +104,47 @@ abstract class CheckServerPresenter constructor(
internal var isNewAccountCreationEnabled = false internal var isNewAccountCreationEnabled = false
internal fun setupConnectionInfo(serverUrl: String) { internal fun setupConnectionInfo(serverUrl: String) {
settingsInteractor?.get(serverUrl)?.let { currentServer = serverUrl
client = factory.create(serverUrl)
managerFactory?.create(serverUrl)?.let {
manager = it
}
dbManagerFactory?.create(serverUrl)?.let {
dbManager = it
}
}
internal suspend fun refreshServerAccounts() {
refreshSettingsInteractor?.refresh(currentServer)
settingsInteractor?.get(currentServer)?.let {
settings = it settings = it
} }
client = factory.create(serverUrl)
state = ""
facebookOauthUrl = null
githubOauthUrl = null
googleOauthUrl = null
linkedinOauthUrl = null
gitlabOauthUrl = null
wordpressOauthUrl = null
casLoginUrl = null
casToken = null
casServiceName = null
casServiceNameTextColor = 0
casServiceButtonColor = 0
customOauthUrl = null
customOauthServiceName = null
customOauthServiceNameTextColor = 0
customOauthServiceButtonColor= 0
samlUrl = null
samlToken = null
samlServiceName = null
samlServiceNameTextColor = 0
samlServiceButtonColor = 0
totalSocialAccountsEnabled = 0
isLoginFormEnabled = false
isNewAccountCreationEnabled = false
} }
internal fun checkServerInfo(serverUrl: String): Job { internal fun checkServerInfo(serverUrl: String): Job {
...@@ -89,28 +155,28 @@ abstract class CheckServerPresenter constructor( ...@@ -89,28 +155,28 @@ abstract class CheckServerPresenter constructor(
client.serverInfo() client.serverInfo()
} }
if (serverInfo.redirected) { if (serverInfo.redirected) {
view?.updateServerUrl(serverInfo.url) versionCheckView?.updateServerUrl(serverInfo.url)
} }
val version = checkServerVersion(serverInfo) val version = checkServerVersion(serverInfo)
when (version) { when (version) {
is Version.VersionOk -> { is Version.VersionOk -> {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})") Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})")
view?.versionOk() versionCheckView?.versionOk()
} }
is Version.RecommendedVersionWarning -> { is Version.RecommendedVersionWarning -> {
Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}") Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}")
view?.alertNotRecommendedVersion() versionCheckView?.alertNotRecommendedVersion()
} }
is Version.OutOfDateError -> { is Version.OutOfDateError -> {
Timber.i("Oops. Looks like your server ${version.version} is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!") Timber.i("Oops. Looks like your server ${version.version} is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
view?.blockAndAlertNotRequiredVersion() versionCheckView?.blockAndAlertNotRequiredVersion()
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error getting server info") Timber.d(ex, "Error getting server info")
when (ex) { when (ex) {
is RocketChatInvalidProtocolException -> view?.errorInvalidProtocol() is RocketChatInvalidProtocolException -> versionCheckView?.errorInvalidProtocol()
else -> view?.errorCheckingServerVersion() else -> versionCheckView?.errorCheckingServerVersion()
} }
} }
} }
...@@ -125,7 +191,7 @@ abstract class CheckServerPresenter constructor( ...@@ -125,7 +191,7 @@ abstract class CheckServerPresenter constructor(
if (services.isNotEmpty()) { if (services.isNotEmpty()) {
state = OauthHelper.getState() state = OauthHelper.getState()
checkEnabledOauthAccounts(services, serverUrl) checkEnabledOauthAccounts(services, serverUrl)
checkEnabledCasAccounts(serverUrl) checkEnabledCasAccounts(services, serverUrl)
checkEnabledCustomOauthAccounts(services, serverUrl) checkEnabledCustomOauthAccounts(services, serverUrl)
checkEnabledSamlAccounts(services, serverUrl) checkEnabledSamlAccounts(services, serverUrl)
} }
...@@ -134,8 +200,59 @@ abstract class CheckServerPresenter constructor( ...@@ -134,8 +200,59 @@ abstract class CheckServerPresenter constructor(
} }
} }
private fun checkEnabledOauthAccounts(services: List<Map<String,Any>>, serverUrl: String) { /**
* Logout the user from the current server.
*
* @param userDataChannel the user data channel to stop listening to changes (if currently subscribed).
*/
internal fun logout(userDataChannel: Channel<Myself>?) {
launchUI(strategy) {
try {
clearTokens()
retryIO("logout") { client.logout() }
} catch (exception: RocketChatException) {
Timber.e(exception, "Error calling logout")
}
try {
if (userDataChannel != null) {
disconnect(userDataChannel)
}
removeAccountInteractor?.remove(currentServer)
tokenRepository?.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
navigator?.switchOrAddNewServer()
} catch (ex: Exception) {
Timber.e(ex, "Error cleaning up the session...")
}
}
}
/**
* Stops listening to user data changes and disconnects the user.
*
* @param userDataChannel the user data channel to stop listening to changes.
*/
fun disconnect(userDataChannel: Channel<Myself>) {
manager.removeUserDataChannel(userDataChannel)
manager.disconnect()
}
private suspend fun clearTokens() {
serverInteractor?.clear()
val pushToken = localRepository?.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
tokenView?.invalidateToken(pushToken)
} catch (ex: Exception) {
Timber.e(ex, "Error unregistering push token")
}
}
localRepository?.clearAllFromServer(currentServer)
}
private fun checkEnabledOauthAccounts(services: List<Map<String,Any>>, serverUrl: String) {
if (settings.isFacebookAuthenticationEnabled()) { if (settings.isFacebookAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_FACEBOOK)?.let { serviceMap -> getServiceMap(services, SERVICE_NAME_FACEBOOK)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId -> getOauthClientId(serviceMap)?.let { clientId ->
...@@ -227,11 +344,25 @@ abstract class CheckServerPresenter constructor( ...@@ -227,11 +344,25 @@ abstract class CheckServerPresenter constructor(
} }
} }
private fun checkEnabledCasAccounts(serverUrl: String) { private fun checkEnabledCasAccounts(services: List<Map<String,Any>>, serverUrl: String) {
if (settings.isCasAuthenticationEnabled()) { if (settings.isCasAuthenticationEnabled()) {
casToken = generateRandomString(17) casToken = generateRandomString(17)
casLoginUrl = settings.casLoginUrl().casUrl(serverUrl, casToken.toString()) casLoginUrl = settings.casLoginUrl().casUrl(serverUrl, casToken.toString())
totalSocialAccountsEnabled++ getCasServices(services).let {
for (serviceMap in it) {
casServiceName = getServiceName(serviceMap)
val serviceNameTextColor = getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (casServiceName != null &&
serviceNameTextColor != null &&
serviceButtonColor != null
) {
casServiceNameTextColor = serviceNameTextColor
casServiceButtonColor = serviceButtonColor
totalSocialAccountsEnabled++
}
}
}
} }
} }
...@@ -244,8 +375,9 @@ abstract class CheckServerPresenter constructor( ...@@ -244,8 +375,9 @@ abstract class CheckServerPresenter constructor(
val clientId = getOauthClientId(serviceMap) val clientId = getOauthClientId(serviceMap)
val scope = getCustomOauthScope(serviceMap) val scope = getCustomOauthScope(serviceMap)
val serviceNameTextColor = val serviceNameTextColor =
getServiceNameColorForCustomOauthOrSaml(serviceMap) getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap) val serviceButtonColor = getServiceButtonColor(serviceMap)
if (customOauthServiceName != null && if (customOauthServiceName != null &&
host != null && host != null &&
authorizePath != null && authorizePath != null &&
...@@ -276,9 +408,9 @@ abstract class CheckServerPresenter constructor( ...@@ -276,9 +408,9 @@ abstract class CheckServerPresenter constructor(
samlToken = generateRandomString(17) samlToken = generateRandomString(17)
for (serviceMap in it) { for (serviceMap in it) {
val provider = getSamlProvider(serviceMap) val provider = getSamlProvider(serviceMap)
samlServiceName = getSamlServiceName(serviceMap) samlServiceName = getServiceName(serviceMap)
val serviceNameTextColor = val serviceNameTextColor =
getServiceNameColorForCustomOauthOrSaml(serviceMap) getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap) val serviceButtonColor = getServiceButtonColor(serviceMap)
if (provider != null && if (provider != null &&
...@@ -369,6 +501,14 @@ abstract class CheckServerPresenter constructor( ...@@ -369,6 +501,14 @@ abstract class CheckServerPresenter constructor(
private fun getCustomOauthServiceName(serviceMap: Map<String, Any>): String? = private fun getCustomOauthServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["service"] as? String serviceMap["service"] as? String
/**
* Returns a CAS service list.
*
* @return A CAS service list, otherwise an empty list if there is no CAS service.
*/
private fun getCasServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> =
listMap.filter { map -> map["service"] == "cas" }
/** /**
* Returns a SAML OAuth service list. * Returns a SAML OAuth service list.
* *
...@@ -388,26 +528,27 @@ abstract class CheckServerPresenter constructor( ...@@ -388,26 +528,27 @@ abstract class CheckServerPresenter constructor(
/** /**
* Returns the text of the SAML service. * Returns the text of the SAML service.
* REMARK: This can be used SAML or CAS.
* *
* @param serviceMap The service map to get the text of the SAML service. * @param serviceMap The service map to get the text of the SAML service.
* @return The text of the SAML service, otherwise null. * @return The text of the SAML service, otherwise null.
*/ */
private fun getSamlServiceName(serviceMap: Map<String, Any>): String? = private fun getServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["buttonLabelText"] as? String serviceMap["buttonLabelText"] as? String
/** /**
* Returns the text color of the service name. * Returns the text color of the service name.
* REMARK: This can be used for custom OAuth or SAML. * REMARK: This can be used for custom OAuth, SAML or CAS.
* *
* @param serviceMap The service map to get the text color from. * @param serviceMap The service map to get the text color from.
* @return The text color of the service (custom OAuth or SAML), otherwise null. * @return The text color of the service (custom OAuth or SAML), otherwise null.
*/ */
private fun getServiceNameColorForCustomOauthOrSaml(serviceMap: Map<String, Any>): Int? = private fun getServiceNameColor(serviceMap: Map<String, Any>): Int? =
(serviceMap["buttonLabelColor"] as? String)?.parseColor() (serviceMap["buttonLabelColor"] as? String)?.parseColor()
/** /**
* Returns the button color of the service name. * Returns the button color of the service name.
* REMARK: This can be used for custom OAuth or SAML. * REMARK: This can be used for custom OAuth, SAML or CAS.
* *
* @param serviceMap The service map to get the button color from. * @param serviceMap The service map to get the button color from.
* @return The button color of the service (custom OAuth or SAML), otherwise null. * @return The button color of the service (custom OAuth or SAML), otherwise null.
...@@ -500,4 +641,4 @@ abstract class CheckServerPresenter constructor( ...@@ -500,4 +641,4 @@ abstract class CheckServerPresenter constructor(
data class OutOfDateError(private val currentVersion: String) : Version(currentVersion) data class OutOfDateError(private val currentVersion: String) : Version(currentVersion)
} }
} }
\ No newline at end of file
package chat.rocket.android.server.presentation
interface TokenView {
fun invalidateToken(token: String)
}
\ No newline at end of file
...@@ -75,6 +75,16 @@ class SettingsFragment : Fragment(), SettingsView, AdapterView.OnItemClickListen ...@@ -75,6 +75,16 @@ class SettingsFragment : Fragment(), SettingsView, AdapterView.OnItemClickListen
AboutFragment.newInstance() AboutFragment.newInstance()
} }
} }
resources.getString(R.string.title_share_the_app) ->{
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
val shareBody = getString(R.string.msg_check_this_out)
val shareSub = getString(R.string.play_store_link)
shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareBody)
shareIntent.putExtra(Intent.EXTRA_TEXT, shareSub)
startActivity(Intent.createChooser(shareIntent, getString(R.string.msg_share_using)))
}
} }
} }
......
package chat.rocket.android.util.extensions package chat.rocket.android.util.extensions
inline fun CharSequence?.isNotNullNorEmpty(block: (CharSequence) -> Unit) { inline fun CharSequence?.ifNotNullNorEmpty(block: (CharSequence) -> Unit) {
if (this != null && this.isNotEmpty()) { if (this != null && this.isNotEmpty()) {
block(this) block(this)
} }
} }
\ No newline at end of file
fun CharSequence?.isNotNullNorEmpty(): Boolean = this != null && this.isNotEmpty()
\ No newline at end of file
...@@ -74,4 +74,6 @@ fun String.lowercaseUrl(): String? { ...@@ -74,4 +74,6 @@ fun String.lowercaseUrl(): String? {
val newScheme = httpUrl?.scheme()?.toLowerCase() val newScheme = httpUrl?.scheme()?.toLowerCase()
return httpUrl?.newBuilder()?.scheme(newScheme)?.build()?.toString() return httpUrl?.newBuilder()?.scheme(newScheme)?.build()?.toString()
} }
\ No newline at end of file
fun String?.isNotNullNorEmpty(): Boolean = this != null && this.isNotEmpty()
\ No newline at end of file
package chat.rocket.android.util.extensions
val <T> T.exhaustive: T
get() = this
\ No newline at end of file
...@@ -4,10 +4,12 @@ import android.net.Uri ...@@ -4,10 +4,12 @@ import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import android.view.View import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import timber.log.Timber import timber.log.Timber
fun View.openTabbedUrl(url: String) { fun View.openTabbedUrl(url: String?) {
if (url == null) return
with(this) { with(this) {
val uri = url.ensureScheme() val uri = url.ensureScheme()
val tabsbuilder = CustomTabsIntent.Builder() val tabsbuilder = CustomTabsIntent.Builder()
...@@ -30,4 +32,27 @@ private fun String.ensureScheme(): Uri? { ...@@ -30,4 +32,27 @@ private fun String.ensureScheme(): Uri? {
} }
return Uri.parse(url.lowercaseUrl()) return Uri.parse(url.lowercaseUrl())
}
inline var List<View>.isVisible: Boolean
get() {
var visible = true
forEach { view ->
if (!view.isVisible) {
visible = false
}
}
return visible
}
set(value) {
forEach { view ->
view.isVisible = value
}
}
fun List<View>.setOnClickListener(listener: (v: View) -> Unit) {
forEach { view ->
view.setOnClickListener(listener)
}
} }
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@color/actionMenuColor"
android:pathData="M9.548,14.23l-2.651,2.652a2.676,2.676 0,0 1,-3.78 0,2.676 2.676,0 0,1 0,-3.78L6.91,9.311a2.677,2.677 0,0 1,3.781 0,0.669 0.669,0 0,0 0.945,-0.946 4.015,4.015 0,0 0,-5.67 0l-3.792,3.792a4.014,4.014 0,0 0,0 5.67,4.014 4.014,0 0,0 5.67,0l2.65,-2.65a0.669,0.669 0,0 0,-0.945 -0.947zM17.828,2.173a4.014,4.014 0,0 0,-5.67 0L9.506,4.824a0.668,0.668 0,1 0,0.946 0.945l2.651,-2.651a2.676,2.676 0,0 1,3.78 0,2.676 2.676,0 0,1 0,3.78L13.09,10.69a2.678,2.678 0,0 1,-3.781 0,0.668 0.668,0 1,0 -0.945,0.945 4.015,4.015 0,0 0,5.67 0l3.793,-3.792a4.014,4.014 0,0 0,0 -5.67z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportHeight="1055.0303" android:viewportWidth="247.7922"
android:viewportWidth="1055.0303"> android:viewportHeight="247.7922">
<group <group
android:translateX="281.28394" android:translateX="69.8961"
android:translateY="271.51514"> android:translateY="69.8961">
<path <path
android:fillColor="#FFDB2323" android:fillType="evenOdd"
android:pathData="M491.3,255.3c0,-24.1 -7.2,-47.2 -21.4,-68.7c-12.8,-19.3 -30.7,-36.4 -53.2,-50.7c-43.5,-27.8 -100.6,-43.1 -160.9,-43.1c-20.1,0 -40,1.7 -59.2,5.1c-11.9,-11.2 -25.9,-21.2 -40.7,-29.2c-79,-38.3 -144.6,-0.9 -144.6,-0.9s60.9,50.1 51,93.9c-27.3,27 -42,59.6 -42,93.6c0,0.1 0,0.2 0,0.3c0,0.1 0,0.2 0,0.3c0,33.9 14.8,66.6 42,93.6c9.9,43.9 -51,93.9 -51,93.9s65.5,37.4 144.6,-0.9c14.8,-8 28.8,-18 40.7,-29.2c19.2,3.4 39.1,5.1 59.2,5.1c60.3,0 117.4,-15.3 160.9,-43.1c22.5,-14.4 40.4,-31.5 53.2,-50.7c14.2,-21.5 21.4,-44.6 21.4,-68.7c0,-0.1 0,-0.2 0,-0.3C491.3,255.6 491.3,255.4 491.3,255.3z" /> android:pathData="M93.921,43.28L93.922,43.283C93.922,43.282 93.922,43.282 93.922,43.282C93.922,43.281 93.921,43.281 93.921,43.28ZM32.924,10.95C36.19,12.769 39.278,15.071 41.914,17.629C46.165,16.857 50.547,16.468 54.993,16.468C68.302,16.468 80.921,19.969 90.522,26.325C95.494,29.618 99.445,33.525 102.266,37.939C105.408,42.857 107,48.145 107,53.812C107,59.327 105.408,64.618 102.266,69.535C99.445,73.951 95.494,77.856 90.522,81.149C80.921,87.505 68.303,91.004 54.993,91.004C50.547,91.004 46.166,90.615 41.914,89.844C39.277,92.401 36.19,94.704 32.924,96.523C15.472,105.288 1,96.729 1,96.729C1,96.729 14.455,85.274 12.267,75.231C6.247,69.043 2.985,61.58 2.985,53.662C2.985,45.893 6.248,38.43 12.267,32.241C14.455,22.201 1.004,10.748 1,10.744C1.004,10.741 15.475,2.186 32.924,10.95Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="54"
android:endY="89.495674"
android:startX="54"
android:startY="-4.593772"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path <path
android:fillColor="#FFFFFF" android:fillColor="#FFFFFF"
android:pathData="M255.9,124.2c113.9,0 206.3,59 206.3,131.8c0,72.8 -92.4,131.8 -206.3,131.8c-25.4,0 -49.7,-2.9 -72.1,-8.3c-22.8,27.4 -73,65.6 -121.7,53.3c15.9,-17 39.4,-45.8 34.3,-93.2c-29.2,-22.7 -46.8,-51.8 -46.8,-83.5C49.6,183.2 142,124.2 255.9,124.2" /> android:fillType="nonZero"
android:pathData="M22.066,71.46C16.128,66.689 12.564,60.582 12.564,53.926C12.564,38.654 31.331,26.273 54.482,26.273C77.633,26.273 96.4,38.654 96.4,53.926C96.4,69.199 77.633,81.58 54.482,81.58C48.776,81.58 43.337,80.828 38.379,79.466L34.754,83.031C32.785,84.968 30.476,86.721 28.07,88.102C24.881,89.699 21.731,90.57 18.615,90.836C18.791,90.51 18.953,90.18 19.127,89.854C22.758,83.031 23.738,76.9 22.066,71.46Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<path <path
android:fillColor="#FFDB2323" android:fillType="nonZero"
android:pathData="M255.9,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" /> android:pathData="M35.209,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="35.2085"
android:endY="58.20792"
android:startX="35.2085"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path <path
android:fillColor="#FFDB2323" android:fillType="nonZero"
android:pathData="M351.2,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" /> android:pathData="M54.482,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="54.4815"
android:endY="58.20792"
android:startX="54.4815"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path <path
android:fillColor="#FFDB2323" android:fillType="nonZero"
android:pathData="M160.6,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" /> android:pathData="M75.682,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="75.6815"
android:endY="58.20792"
android:startX="75.6815"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</group> </group>
</vector> </vector>
\ No newline at end of file
...@@ -133,15 +133,6 @@ ...@@ -133,15 +133,6 @@
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<Button
android:id="@+id/button_cas"
style="@style/Authentication.Button"
android:layout_marginTop="10dp"
android:clickable="false"
android:text="@string/action_login_or_sign_up"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/attachment_container" android:id="@+id/attachment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
...@@ -10,20 +11,112 @@ ...@@ -10,20 +11,112 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:paddingStart="@dimen/screen_edge_left_and_right_padding" android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding" android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"> android:paddingBottom="@dimen/message_item_top_and_bottom_padding">
<View <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="56dp" />
<!-- File attachments (image, video, audio) -->
<TextView
android:id="@+id/file_name"
style="@style/Message.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideline"
tools:text="Filename.png"
tools:visibility="visible"/>
<TextView
android:id="@+id/file_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_name"
tools:text="Some description"
tools:visibility="visible"/>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/file_description"
fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/image_dummy"
tools:background="@drawable/image_dummy"
tools:visibility="visible"/>
<FrameLayout
android:id="@+id/audio_video_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:background="@color/colorBlack"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_attachment"
tools:visibility="gone">
<ImageView
android:id="@+id/play_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/exo_controls_play" />
</FrameLayout>
<TextView
android:id="@+id/file_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/audio_video_attachment"
tools:text="Some text"
tools:visibility="visible"/>
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:drawableStart="@drawable/ic_files_24dp"
android:drawablePadding="6dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_text"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf"
tools:visibility="visible" />
<!-- END File attachments -->
<ImageView
android:id="@+id/quote_bar" android:id="@+id/quote_bar"
android:layout_width="4dp" android:layout_width="4dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="56dp" android:src="@drawable/quote_vertical_gray_bar"
android:background="@drawable/quote_vertical_gray_bar" android:scaleType="fitXY"
app:layout_constraintBottom_toTopOf="@+id/text_view_more" android:visibility="gone"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/actions_list"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_file_name"
tools:visibility="visible"/>
<!-- Message attachment -->
<TextView <TextView
android:id="@+id/text_sender" android:id="@+id/text_sender"
style="@style/Sender.Name.TextView" style="@style/Sender.Name.TextView"
...@@ -31,8 +124,9 @@ ...@@ -31,8 +124,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:textColor="@color/colorPrimary" android:textColor="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/quote_bar" app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/text_file_name"
tools:text="Ronald Perkins" /> tools:text="Ronald Perkins" />
<TextView <TextView
...@@ -41,6 +135,7 @@ ...@@ -41,6 +135,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/text_sender" app:layout_constraintBottom_toBottomOf="@+id/text_sender"
app:layout_constraintStart_toEndOf="@+id/text_sender" app:layout_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender" app:layout_constraintTop_toTopOf="@+id/text_sender"
...@@ -51,6 +146,7 @@ ...@@ -51,6 +146,7 @@
style="@style/Message.Quote.TextView" style="@style/Message.Quote.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/text_view_more" app:layout_constraintBottom_toTopOf="@id/text_view_more"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_sender" app:layout_constraintStart_toStartOf="@+id/text_sender"
...@@ -64,16 +160,105 @@ ...@@ -64,16 +160,105 @@
android:gravity="end" android:gravity="end"
android:textColor="@color/darkGray" android:textColor="@color/darkGray"
android:textSize="14sp" android:textSize="14sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/text_content" app:layout_constraintEnd_toEndOf="@+id/text_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar" app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@+id/text_content" app:layout_constraintTop_toBottomOf="@+id/text_content"
tools:text="Visualizar mais" /> tools:text="Visualizar mais" />
<!-- END Message attachment -->
<!-- author -->
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/author_icon"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginTop="6dp"
android:layout_marginStart="8dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
app:layout_constraintStart_toStartOf="@id/guideline"
tools:src="@tools:sample/avatars"
tools:visibility="visible"/>
<TextView
android:id="@+id/text_author_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
app:layout_constraintStart_toEndOf="@id/author_icon"
app:layout_constraintEnd_toEndOf="parent"
tools:text="#5571 - User profile from SSO must not have password change option"
tools:visibility="visible"/>
<!-- END author -->
<!-- TEXT -->
<TextView
android:id="@+id/file_name_not_file_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_author_name"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf"
tools:visibility="visible" />
<TextView
android:id="@+id/attachment_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/file_name_not_file_type"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
tools:text="#5571 - User profile from SSO must not have password change option"
tools:visibility="visible"/>
<!-- END TEXT -->
<!-- Fields -->
<TextView
android:id="@+id/text_fields"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/attachment_text"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
tools:text="line1\nline2\n\nline3\nline4"
tools:visibility="visible"/>
<!-- END Fields -->
<!-- Actions -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/actions_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:textDirection="locale"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_fields" />
<include <include
layout="@layout/layout_reactions" layout="@layout/layout_reactions"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar" app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@+id/text_view_more" /> app:layout_constraintTop_toBottomOf="@+id/actions_list" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_gray_bar"
app:layout_constraintBottom_toTopOf="@+id/text_view_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorPrimary"
app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"
app:layout_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender"
tools:text="11:45 PM" />
<TextView
android:id="@+id/text_content"
style="@style/Message.Quote.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/text_view_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+id/text_sender"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<TextView
android:id="@+id/text_view_more"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:textColor="@color/darkGray"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="@+id/text_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@+id/text_content"
tools:text="Visualizar mais" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@+id/text_view_more" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_update_profile"
android:icon="@drawable/ic_check_white_24dp"
android:title="@string/action_update" />
</menu>
\ No newline at end of file
...@@ -12,6 +12,11 @@ ...@@ -12,6 +12,11 @@
android:icon="@drawable/ic_action_message_reply_24dp" android:icon="@drawable/ic_action_message_reply_24dp"
android:title="@string/action_msg_reply" /> android:title="@string/action_msg_reply" />
<item
android:id="@+id/action_message_permalink"
android:icon="@drawable/ic_action_message_link_24dp"
android:title="@string/action_msg_copy_permalink" />
<item <item
android:id="@+id/action_message_quote" android:id="@+id/action_message_quote"
android:icon="@drawable/ic_action_message_quote_24dp" android:icon="@drawable/ic_action_message_quote_24dp"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_update_profile" android:id="@+id/action_delete_account"
android:icon="@drawable/ic_check_white_24dp" android:title="@string/action_delete_account"
android:title="@string/action_update" /> app:showAsAction="never" />
</menu> </menu>
\ No newline at end of file
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Anmelden am Server</string> <string name="title_sign_in_your_server">Anmelden am Server</string>
<string name="title_log_in">Anmelden</string> <string name="title_log_in">Anmelden</string>
<string name="title_share_the_app">App teilen</string>
<string name="title_register_username">Registriere Benutzernamen</string> <string name="title_register_username">Registriere Benutzernamen</string>
<string name="title_reset_password">Passwort zurücksetzen</string> <string name="title_reset_password">Passwort zurücksetzen</string>
<string name="title_sign_up">registrieren</string> <string name="title_sign_up">registrieren</string>
...@@ -20,12 +21,11 @@ ...@@ -20,12 +21,11 @@
<string name="title_update_profile">Update Profil</string> <string name="title_update_profile">Update Profil</string>
<string name="title_about">Über</string> <string name="title_about">Über</string>
<string name="title_create_channel">Erstelle Raum</string> <string name="title_create_channel">Erstelle Raum</string>
<string name="title_confirmation">Are You Sure you want to logout?</string><!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string><!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Verbinde</string> <string name="action_connect">Verbinde</string>
<string name="action_use_this_username">Benutze den Benutzernamen</string> <string name="action_use_this_username">Benutze den Benutzernamen</string>
<string name="action_login_or_sign_up">Klick diesen Knopf um sich anzumelden oder einen Account zu erstellen</string>
<string name="action_terms_of_service">Nutzungsbedingungen</string> <string name="action_terms_of_service">Nutzungsbedingungen</string>
<string name="action_privacy_policy">Datenschutz</string> <string name="action_privacy_policy">Datenschutz</string>
<string name="action_search">Suche</string> <string name="action_search">Suche</string>
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
<string name="action_create_channel">Erstelle Raum</string> <string name="action_create_channel">Erstelle Raum</string>
<string name="action_create">Erstelle</string> <string name="action_create">Erstelle</string>
<string name="action_logout">Abmelden</string> <string name="action_logout">Abmelden</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Dateien</string> <string name="action_files">Dateien</string>
<string name="action_confirm_password">Bestätige Passwort Änderung</string> <string name="action_confirm_password">Bestätige Passwort Änderung</string>
<string name="action_join_chat">Trete Chat bei</string> <string name="action_join_chat">Trete Chat bei</string>
...@@ -53,17 +52,21 @@ ...@@ -53,17 +52,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferences</item> <!-- TODO Add translation --> <item name="item_preferences">Preferences</item> <!-- TODO Add translation -->
<item name="item_password">Ändere Passwort</item> <item name="item_password">Ändere Passwort</item>
<item name="item_password">Über</item> <item name="item_password">Über</item>
<item name="item_share_app">App teilen</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Entschuldigung, ein Fehler ist aufgetreten, bitte versuchen Sie es noch einmal.</string> <string name="msg_generic_error">Entschuldigung, ein Fehler ist aufgetreten, bitte versuchen Sie es noch einmal.</string>
<string name="msg_no_data_to_display">Keine Anzeigedaten vorhanden</string> <string name="msg_no_data_to_display">Keine Anzeigedaten vorhanden</string>
<string name="msg_check_this_out">Schau dir das an</string>
<string name="msg_share_using">Teilen Sie mit</string>
<string name="msg_profile_update_successfully">Profil update erfolgreich</string> <string name="msg_profile_update_successfully">Profil update erfolgreich</string>
<string name="msg_username">Benutzername</string> <string name="msg_username">Benutzername</string>
<string name="msg_username_or_email">Benutzername oder E-Mail</string> <string name="msg_username_or_email">Benutzername oder E-Mail</string>
...@@ -145,6 +148,8 @@ ...@@ -145,6 +148,8 @@
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation -->
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation --> <string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation -->
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation --> <string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Privat</string> <string name="msg_private_channel">Privat</string>
...@@ -164,6 +169,8 @@ ...@@ -164,6 +169,8 @@
<string name="msg_view_more">view more</string> <string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation --> <!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string> <string name="msg_view_less">view less</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation --> <string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
...@@ -200,6 +207,8 @@ ...@@ -200,6 +207,8 @@
<string name="action_msg_share">Teilen</string> <string name="action_msg_share">Teilen</string>
<string name="action_title_editing">Nachricht bearbeiten</string> <string name="action_title_editing">Nachricht bearbeiten</string>
<string name="action_msg_add_reaction">Reaktion hinzufügen</string> <string name="action_msg_add_reaction">Reaktion hinzufügen</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Bearbeiten nicht erlaubt</string> <string name="permission_editing_not_allowed">Bearbeiten nicht erlaubt</string>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Inicia sesión en tu servidor</string> <string name="title_sign_in_your_server">Inicia sesión en tu servidor</string>
<string name="title_log_in">Iniciar sesión</string> <string name="title_log_in">Iniciar sesión</string>
<string name="title_share_the_app">Compartir aplicación</string>
<string name="title_register_username">Registrar nombre de usuario</string> <string name="title_register_username">Registrar nombre de usuario</string>
<string name="title_reset_password">Restablecer la contraseña</string> <string name="title_reset_password">Restablecer la contraseña</string>
<string name="title_sign_up">Regístrate</string> <string name="title_sign_up">Regístrate</string>
...@@ -19,11 +20,11 @@ ...@@ -19,11 +20,11 @@
<string name="title_update_profile">Actualización del perfil</string> <string name="title_update_profile">Actualización del perfil</string>
<string name="title_about">Acerca de</string> <string name="title_about">Acerca de</string>
<string name="title_create_channel">Crear canal</string> <string name="title_create_channel">Crear canal</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Conectar</string> <string name="action_connect">Conectar</string>
<string name="action_use_this_username">Usa este nombre de usuario</string> <string name="action_use_this_username">Usa este nombre de usuario</string>
<string name="action_login_or_sign_up">Toca en este botón para iniciar sesión o crear una cuenta</string>
<string name="action_terms_of_service">Términos de Servicio</string> <string name="action_terms_of_service">Términos de Servicio</string>
<string name="action_privacy_policy">Política de Privacidad</string> <string name="action_privacy_policy">Política de Privacidad</string>
<string name="action_search">Buscar</string> <string name="action_search">Buscar</string>
...@@ -32,7 +33,6 @@ ...@@ -32,7 +33,6 @@
<string name="action_create_channel">Crear canal</string> <string name="action_create_channel">Crear canal</string>
<string name="action_create">Create</string> <string name="action_create">Create</string>
<string name="action_logout">Cerrar sesión</string> <string name="action_logout">Cerrar sesión</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Archivos</string> <string name="action_files">Archivos</string>
<string name="action_confirm_password">Confirmar cambio de contraseña</string> <string name="action_confirm_password">Confirmar cambio de contraseña</string>
<string name="action_join_chat">Unirse al chat</string> <string name="action_join_chat">Unirse al chat</string>
...@@ -51,17 +51,21 @@ ...@@ -51,17 +51,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferences</item> <!-- TODO Add translation --> <item name="item_preferences">Preferences</item> <!-- TODO Add translation -->
<item name="item_password">Cambia la contraseña</item> <item name="item_password">Cambia la contraseña</item>
<item name="item_password">Acerca de</item> <item name="item_password">Acerca de</item>
<item name="item_share_app">Compartir aplicación</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Lo sentimos, ha ocurrido un error, por favor intente de nuevo</string> <string name="msg_generic_error">Lo sentimos, ha ocurrido un error, por favor intente de nuevo</string>
<string name="msg_no_data_to_display">No hay información para mostrar</string> <string name="msg_no_data_to_display">No hay información para mostrar</string>
<string name="msg_check_this_out">Mira esto</string>
<string name="msg_share_using">Compartir usando</string>
<string name="msg_profile_update_successfully">Actualización de perfil con éxito</string> <string name="msg_profile_update_successfully">Actualización de perfil con éxito</string>
<string name="msg_username">usuario</string> <string name="msg_username">usuario</string>
<string name="msg_username_or_email">Nombre de usuario o correo electrónico</string> <string name="msg_username_or_email">Nombre de usuario o correo electrónico</string>
...@@ -160,6 +164,10 @@ ...@@ -160,6 +164,10 @@
<string name="msg_view_more">view more</string> <string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation --> <!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string> <string name="msg_view_less">view less</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation --> <string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
...@@ -196,6 +204,7 @@ ...@@ -196,6 +204,7 @@
<string name="action_msg_share">Compartir</string> <string name="action_msg_share">Compartir</string>
<string name="action_title_editing">Edición de mensaje</string> <string name="action_title_editing">Edición de mensaje</string>
<string name="action_msg_add_reaction">Añadir una reacción</string> <string name="action_msg_add_reaction">Añadir una reacción</string>
<string name="action_msg_copy_permalink">Copiar permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">La edición no és permitida</string> <string name="permission_editing_not_allowed">La edición no és permitida</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Connectez-vous sur votre serveur</string> <string name="title_sign_in_your_server">Connectez-vous sur votre serveur</string>
<string name="title_log_in">S\'identifier</string> <string name="title_log_in">S\'identifier</string>
<string name="title_share_the_app">Partager l\'application</string>
<string name="title_register_username">Enregistrer le nom d\'utilisateur</string> <string name="title_register_username">Enregistrer le nom d\'utilisateur</string>
<string name="title_reset_password">Réinitialiser mot de passe</string> <string name="title_reset_password">Réinitialiser mot de passe</string>
<string name="title_sign_up">S\'inscrire</string> <string name="title_sign_up">S\'inscrire</string>
...@@ -20,14 +21,11 @@ ...@@ -20,14 +21,11 @@
<string name="title_update_profile">Mettre à jour le profil</string> <string name="title_update_profile">Mettre à jour le profil</string>
<string name="title_about">À propos</string> <string name="title_about">À propos</string>
<string name="title_create_channel">Créer salon</string> <string name="title_create_channel">Créer salon</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Se connecter</string> <string name="action_connect">Se connecter</string>
<string name="action_use_this_username">Utilisez ce nom d\'utilisateur</string> <string name="action_use_this_username">Utilisez ce nom d\'utilisateur</string>
<string name="action_login_or_sign_up">Touchez ce bouton pour vous connecter ou créer un compte</string>
<string name="action_terms_of_service">Conditions d\'utilisation</string> <string name="action_terms_of_service">Conditions d\'utilisation</string>
<string name="action_privacy_policy">Politique de confidentialité</string> <string name="action_privacy_policy">Politique de confidentialité</string>
<string name="action_search">Chercher</string> <string name="action_search">Chercher</string>
...@@ -36,7 +34,6 @@ ...@@ -36,7 +34,6 @@
<string name="action_create_channel">Créer salon</string> <string name="action_create_channel">Créer salon</string>
<string name="action_create">Créer</string> <string name="action_create">Créer</string>
<string name="action_logout">Se déconnecter</string> <string name="action_logout">Se déconnecter</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Fichiers</string> <string name="action_files">Fichiers</string>
<string name="action_confirm_password">Confirmer le mot de passe</string> <string name="action_confirm_password">Confirmer le mot de passe</string>
<string name="action_join_chat">Rejoignez le chat</string> <string name="action_join_chat">Rejoignez le chat</string>
...@@ -55,17 +52,21 @@ ...@@ -55,17 +52,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Préférences</item> <item name="item_preferences">Préférences</item>
<item name="item_password">Changer le mot de passe</item> <item name="item_password">Changer le mot de passe</item>
<item name="item_password">À propos</item> <item name="item_password">À propos</item>
<item name="item_share_app">Partager l\'application</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Désolé, une erreur s\'est produite. Veuillez réessayer</string> <string name="msg_generic_error">Désolé, une erreur s\'est produite. Veuillez réessayer</string>
<string name="msg_no_data_to_display">Aucune donnée à afficher</string> <string name="msg_no_data_to_display">Aucune donnée à afficher</string>
<string name="msg_check_this_out">regarde ça</string>
<string name="msg_share_using">Partager en utilisant</string>
<string name="msg_profile_update_successfully">Mise à jour du profil avec succès</string> <string name="msg_profile_update_successfully">Mise à jour du profil avec succès</string>
<string name="msg_username">nom d\'utilisateur</string> <string name="msg_username">nom d\'utilisateur</string>
<string name="msg_username_or_email">Nom d\'utilisateur ou email</string> <string name="msg_username_or_email">Nom d\'utilisateur ou email</string>
...@@ -155,6 +156,10 @@ ...@@ -155,6 +156,10 @@
<string name="msg_view_more">view more</string> <string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation --> <!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string> <string name="msg_view_less">view less</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Privé</string> <string name="msg_private_channel">Privé</string>
...@@ -204,6 +209,8 @@ ...@@ -204,6 +209,8 @@
<string name="action_msg_share">Partager</string> <string name="action_msg_share">Partager</string>
<string name="action_title_editing">Modification du message</string> <string name="action_title_editing">Modification du message</string>
<string name="action_msg_add_reaction">Ajouter une réaction</string> <string name="action_msg_add_reaction">Ajouter une réaction</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">L\'édition n\'est pas autorisée</string> <string name="permission_editing_not_allowed">L\'édition n\'est pas autorisée</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">अपने सर्वर में साइन इन करें</string> <string name="title_sign_in_your_server">अपने सर्वर में साइन इन करें</string>
<string name="title_log_in">लॉग इन करें</string> <string name="title_log_in">लॉग इन करें</string>
<string name="title_share_the_app">ऐप शेयर करे</string>
<string name="title_register_username">रजिस्टर उपयोगकर्ता नाम</string> <string name="title_register_username">रजिस्टर उपयोगकर्ता नाम</string>
<string name="title_reset_password">पासवर्ड रीसेट करें</string> <string name="title_reset_password">पासवर्ड रीसेट करें</string>
<string name="title_sign_up">साइन अप करें</string> <string name="title_sign_up">साइन अप करें</string>
...@@ -20,13 +21,11 @@ ...@@ -20,13 +21,11 @@
<string name="title_update_profile">प्रोफ़ाइल अपडेट करें</string> <string name="title_update_profile">प्रोफ़ाइल अपडेट करें</string>
<string name="title_about">परिचय</string> <string name="title_about">परिचय</string>
<string name="title_create_channel">चैनल बनाएं</string> <string name="title_create_channel">चैनल बनाएं</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">जुडिये</string> <string name="action_connect">जुडिये</string>
<string name="action_use_this_username">इस उपयोगकर्ता नाम का उपयोग करें</string> <string name="action_use_this_username">इस उपयोगकर्ता नाम का उपयोग करें</string>
<string name="action_login_or_sign_up">लॉग इन करने या खाता बनाने के लिए इस बटन को टैप करें</string>
<string name="action_terms_of_service">सेवा की शर्तें</string> <string name="action_terms_of_service">सेवा की शर्तें</string>
<string name="action_privacy_policy">गोपनीयता नीति</string> <string name="action_privacy_policy">गोपनीयता नीति</string>
<string name="action_search">खोजें</string> <string name="action_search">खोजें</string>
...@@ -35,7 +34,6 @@ ...@@ -35,7 +34,6 @@
<string name="action_create_channel">चैनल बनाएं</string> <string name="action_create_channel">चैनल बनाएं</string>
<string name="action_create">बनाएं</string> <string name="action_create">बनाएं</string>
<string name="action_logout">लोग आउट करें</string> <string name="action_logout">लोग आउट करें</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">फ़ाइलें</string> <string name="action_files">फ़ाइलें</string>
<string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string> <string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string>
<string name="action_join_chat">चैट में शामिल हों</string> <string name="action_join_chat">चैट में शामिल हों</string>
...@@ -54,17 +52,21 @@ ...@@ -54,17 +52,21 @@
<string name="action_create_server">नया सर्वर बनाएं</string> <string name="action_create_server">नया सर्वर बनाएं</string>
<string name="action_register">रजिस्टर</string> <string name="action_register">रजिस्टर</string>
<string name="action_confirm">पुष्टि करें</string> <string name="action_confirm">पुष्टि करें</string>
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">प्राथमिकताएँ</item> <item name="item_preferences">प्राथमिकताएँ</item>
<item name="item_password">पासवर्ड बदलें</item> <item name="item_password">पासवर्ड बदलें</item>
<item name="item_password">परिचय</item> <item name="item_password">परिचय</item>
<item name="item_share_app">ऐप शेयर करें</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">क्षमा करें, एक त्रुटि हुई है, कृपया पुनः प्रयास करें</string> <string name="msg_generic_error">क्षमा करें, एक त्रुटि हुई है, कृपया पुनः प्रयास करें</string>
<string name="msg_no_data_to_display">डेटा प्रदर्शित करने के लिए उपलब्ध नहीं हैं</string> <string name="msg_no_data_to_display">डेटा प्रदर्शित करने के लिए उपलब्ध नहीं हैं</string>
<string name="msg_check_this_out">इसकी जांच करें</string>
<string name="msg_share_using">उपयोग कर साझा करें</string>
<string name="msg_profile_update_successfully">प्रोफ़ाइल सफलतापूर्वक अपडेट हो गया है</string> <string name="msg_profile_update_successfully">प्रोफ़ाइल सफलतापूर्वक अपडेट हो गया है</string>
<string name="msg_username">यूजरनेम</string> <string name="msg_username">यूजरनेम</string>
<string name="msg_username_or_email">यूजरनेम या ईमेल</string> <string name="msg_username_or_email">यूजरनेम या ईमेल</string>
...@@ -166,6 +168,10 @@ ...@@ -166,6 +168,10 @@
<string name="msg_channel_created_successfully">चैनल सफलतापूर्वक बनाया गया</string> <string name="msg_channel_created_successfully">चैनल सफलतापूर्वक बनाया गया</string>
<string name="msg_view_more">और देखें</string> <string name="msg_view_more">और देखें</string>
<string name="msg_view_less">कम देखें</string> <string name="msg_view_less">कम देखें</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string> <string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string>
...@@ -188,7 +194,6 @@ ...@@ -188,7 +194,6 @@
<string name="message_role_removed">%1$s अब %3$s द्वारा %2$s नहीं है</string> <string name="message_role_removed">%1$s अब %3$s द्वारा %2$s नहीं है</string>
<string name="message_credentials_saved_successfully">प्रमाण पत्र सफलतापूर्वक सहेजे गए</string> <string name="message_credentials_saved_successfully">प्रमाण पत्र सफलतापूर्वक सहेजे गए</string>
<!-- Message actions --> <!-- Message actions -->
<string name="action_msg_reply">जवाब दें</string> <string name="action_msg_reply">जवाब दें</string>
<string name="action_msg_info">संदेश जानकारी</string> <string name="action_msg_info">संदेश जानकारी</string>
...@@ -203,6 +208,8 @@ ...@@ -203,6 +208,8 @@
<string name="action_msg_share">शेयर करें</string> <string name="action_msg_share">शेयर करें</string>
<string name="action_title_editing">संपादन संदेश</string> <string name="action_title_editing">संपादन संदेश</string>
<string name="action_msg_add_reaction">प्रतिक्रिया जोड़ें</string> <string name="action_msg_add_reaction">प्रतिक्रिया जोड़ें</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">संपादन की अनुमति नहीं है</string> <string name="permission_editing_not_allowed">संपादन की अनुमति नहीं है</string>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">サーバーに接続</string> <string name="title_sign_in_your_server">サーバーに接続</string>
<string name="title_log_in">ログイン</string> <string name="title_log_in">ログイン</string>
<string name="title_share_the_app">アプリを共有する</string>
<string name="title_register_username">ユーザー名を登録する</string> <string name="title_register_username">ユーザー名を登録する</string>
<string name="title_reset_password">パスワードリセット</string> <string name="title_reset_password">パスワードリセット</string>
<string name="title_sign_up">サインアップ</string> <string name="title_sign_up">サインアップ</string>
...@@ -22,13 +23,11 @@ ...@@ -22,13 +23,11 @@
<string name="title_update_profile">プロフィールの更新</string> <string name="title_update_profile">プロフィールの更新</string>
<string name="title_about">About</string> <string name="title_about">About</string>
<string name="title_create_channel">新しいチャネルを作成します</string> <string name="title_create_channel">新しいチャネルを作成します</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">接続</string> <string name="action_connect">接続</string>
<string name="action_use_this_username">このユーザー名を使用する</string> <string name="action_use_this_username">このユーザー名を使用する</string>
<string name="action_login_or_sign_up">ログインまたはアカウントを作成するにはこのボタンを押してください</string>
<string name="action_terms_of_service">サービス利用規約</string> <string name="action_terms_of_service">サービス利用規約</string>
<string name="action_privacy_policy">プライバシーポリシー</string> <string name="action_privacy_policy">プライバシーポリシー</string>
<string name="action_search">検索</string> <string name="action_search">検索</string>
...@@ -37,7 +36,6 @@ ...@@ -37,7 +36,6 @@
<string name="action_create_channel">チャンネル作成</string> <string name="action_create_channel">チャンネル作成</string>
<string name="action_create">作ります</string> <string name="action_create">作ります</string>
<string name="action_logout">ログアウト</string> <string name="action_logout">ログアウト</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">ファイル</string> <string name="action_files">ファイル</string>
<string name="action_confirm_password">変更したパスワードの確認</string> <string name="action_confirm_password">変更したパスワードの確認</string>
<string name="action_join_chat">チャットに参加</string> <string name="action_join_chat">チャットに参加</string>
...@@ -56,17 +54,21 @@ ...@@ -56,17 +54,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferences</item> <!-- TODO Add translation --> <item name="item_preferences">Preferences</item> <!-- TODO Add translation -->
<item name="item_password">Change Password</item> <!-- TODO Add translation --> <item name="item_password">Change Password</item> <!-- TODO Add translation -->
<item name="item_password">About</item> <!-- TODO Add translation --> <item name="item_password">About</item> <!-- TODO Add translation -->
<item name="item_share_app">アプリを共有する</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">エラーが発生しました。もう一度お試しください。</string> <string name="msg_generic_error">エラーが発生しました。もう一度お試しください。</string>
<string name="msg_no_data_to_display">表示するデータがありません</string> <string name="msg_no_data_to_display">表示するデータがありません</string>
<string name="msg_check_this_out">これをチェックする</string>
<string name="msg_share_using">共有する</string>
<string name="msg_profile_update_successfully">プロフィールの更新に成功しました</string> <string name="msg_profile_update_successfully">プロフィールの更新に成功しました</string>
<string name="msg_username">ユーザー名</string> <string name="msg_username">ユーザー名</string>
<string name="msg_username_or_email">メールアドレスまたはユーザー名</string> <string name="msg_username_or_email">メールアドレスまたはユーザー名</string>
...@@ -139,22 +141,25 @@ ...@@ -139,22 +141,25 @@
<string name="msg_file_description">ファイルの説明</string> <string name="msg_file_description">ファイルの説明</string>
<string name="msg_send">送信</string> <string name="msg_send">送信</string>
<string name="msg_sent_attachment">添付ファイルを送信しました</string> <string name="msg_sent_attachment">添付ファイルを送信しました</string>
<string name="msg_welcome_to_rocket_chat">Welcome to Rocket.Chat</string> <!-- TODO Add translation -->
<string name="msg_team_communication">Team Communication</string> <!-- TODO Add translation --> <string name="msg_welcome_to_rocket_chat">Rocket.Chatへようこそ</string>
<string name="msg_login_with_email">Login with <b>e-mail</b></string> <!-- TODO Add translation --> <string name="msg_team_communication">チームコミュニケーション</string>
<string name="msg_create_account">Create an account</string> <!-- TODO Add translation --> <string name="msg_login_with_email"><b>e-mail</b>でログイン</string>
<string name="msg_continue_with_facebook">Continue with <b>Facebook</b></string> <!-- TODO Add translation --> <string name="msg_create_account">アカウントを作成</string>
<string name="msg_continue_with_github">Continue with <b>Github</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_facebook"><b>Facebook</b>でログイン</string>
<string name="msg_continue_with_google">Continue with <b>Google</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_github"><b>Github</b>でログイン</string>
<string name="msg_continue_with_linkedin">Continue with <b>Linkedin</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_google"><b>Google</b>でログイン</string>
<string name="msg_continue_with_gitlab">Continue with <b>GitLab</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_linkedin"><b>Linkedin</b>でログイン</string>
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <!-- TODO Add translation --> <string name="msg_continue_with_gitlab"><b>GitLab</b>でログイン</string>
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <!-- TODO Add translation --> <string name="msg_continue_with_wordpress"><b>WordPress</b>でログイン</string>
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation --> <string name="msg_two_factor_authentication">二要素認証</string>
<!-- TODO - Add proper translation --> <string name="msg__your_2fa_code">あなたの 2FA コードは何ですか?</string>
<string name="msg_view_more">view more</string>
<string name="msg_view_more">更に表示</string>
<string name="msg_view_less">隠す</string>
<!-- TODO - Add proper translation --> <!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string> <string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">プライベート</string> <string name="msg_private_channel">プライベート</string>
...@@ -170,6 +175,8 @@ ...@@ -170,6 +175,8 @@
<string name="msg_message_copied">メッセージをコピー</string> <string name="msg_message_copied">メッセージをコピー</string>
<string name="msg_delete_message">メッセージを削除</string> <string name="msg_delete_message">メッセージを削除</string>
<string name="msg_delete_description">このメッセージを削除してもよろしいですか?</string> <string name="msg_delete_description">このメッセージを削除してもよろしいですか?</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation --> <string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
...@@ -206,6 +213,8 @@ ...@@ -206,6 +213,8 @@
<string name="action_msg_share">Share</string> <string name="action_msg_share">Share</string>
<string name="action_title_editing">メッセージの編集</string> <string name="action_title_editing">メッセージの編集</string>
<string name="action_msg_add_reaction">リアクションする</string> <string name="action_msg_add_reaction">リアクションする</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">編集は許可されていません</string> <string name="permission_editing_not_allowed">編集は許可されていません</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Faça login no seu servidor</string> <string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</string> <string name="title_log_in">Entrar</string>
<string name="title_share_the_app">Compartilhe o aplicativo</string>
<string name="title_register_username">Registre o nome de usuário</string> <string name="title_register_username">Registre o nome de usuário</string>
<string name="title_reset_password">Redefinir senha</string> <string name="title_reset_password">Redefinir senha</string>
<string name="title_sign_up">Inscreva-se</string> <string name="title_sign_up">Inscreva-se</string>
...@@ -20,13 +21,11 @@ ...@@ -20,13 +21,11 @@
<string name="title_update_profile">Editar perfil</string> <string name="title_update_profile">Editar perfil</string>
<string name="title_about">Sobre</string> <string name="title_about">Sobre</string>
<string name="title_create_channel">Criar chat</string> <string name="title_create_channel">Criar chat</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Você tem certeza?</string>
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Conectar</string> <string name="action_connect">Conectar</string>
<string name="action_use_this_username">Usar este nome de usuário</string> <string name="action_use_this_username">Usar este nome de usuário</string>
<string name="action_login_or_sign_up">Toque neste botão para fazer login ou criar uma conta</string>
<string name="action_terms_of_service">Termos de Serviço</string> <string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string> <string name="action_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string> <string name="action_search">Pesquisar</string>
...@@ -35,7 +34,6 @@ ...@@ -35,7 +34,6 @@
<string name="action_create_channel">Criar chat</string> <string name="action_create_channel">Criar chat</string>
<string name="action_create">Criar</string> <string name="action_create">Criar</string>
<string name="action_logout">Sair</string> <string name="action_logout">Sair</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Arquivos</string> <string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string> <string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string> <string name="action_join_chat">Entrar no Chat</string>
...@@ -49,22 +47,26 @@ ...@@ -49,22 +47,26 @@
<string name="action_select_photo_from_gallery">Escolher foto da galeria</string> <string name="action_select_photo_from_gallery">Escolher foto da galeria</string>
<string name="action_take_photo">Tirar foto</string> <string name="action_take_photo">Tirar foto</string>
<string name="action_reset_avatar">Resetar avatar</string> <string name="action_reset_avatar">Resetar avatar</string>
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation --> <string name="action_connect_server">Conectar com um servidor</string>
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation --> <string name="action_join_community">Junte-se à comunidade</string>
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Criar um novo servidor</string>
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Registrar</string>
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirmar</string>
<string name="action_delete_account">Deletar conta</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferencias</item> <item name="item_preferences">Preferencias</item>
<item name="item_password">Alterar senha</item> <item name="item_password">Alterar senha</item>
<item name="item_password">Sobre</item> <item name="item_password">Sobre</item>
<item name="item_share_app">Compartilhe o aplicativo</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string> <string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string>
<string name="msg_no_data_to_display">Nenhum dado para exibir</string> <string name="msg_no_data_to_display">Nenhum dado para exibir</string>
<string name="msg_check_this_out">Veja isso</string>
<string name="msg_share_using">compartilhar usando</string>
<string name="msg_profile_update_successfully">Perfil atualizado com sucesso</string> <string name="msg_profile_update_successfully">Perfil atualizado com sucesso</string>
<string name="msg_username">nome de usuário</string> <string name="msg_username">nome de usuário</string>
<string name="msg_username_or_email">Nome de usuário ou email</string> <string name="msg_username_or_email">Nome de usuário ou email</string>
...@@ -153,6 +155,9 @@ ...@@ -153,6 +155,9 @@
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation --> <string name="msg__your_2fa_code">What’s your 2FA code?</string> <!-- TODO Add translation -->
<string name="msg_view_more">visualizar mais</string> <string name="msg_view_more">visualizar mais</string>
<string name="msg_view_less">visualizar menos</string> <string name="msg_view_less">visualizar menos</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copiado</string>
<string name="msg_muted_on_this_channel">Você está silenciado neste canal</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Privado</string> <string name="msg_private_channel">Privado</string>
...@@ -181,8 +186,8 @@ ...@@ -181,8 +186,8 @@
<string name="message_welcome">Bem-vindo, %s</string> <string name="message_welcome">Bem-vindo, %s</string>
<string name="message_removed">Mensagem removida</string> <string name="message_removed">Mensagem removida</string>
<string name="message_pinned">Pinou uma mensagem:</string> <string name="message_pinned">Pinou uma mensagem:</string>
<string name="message_muted">Usuário %1$s entrou no modo mudo por %2$s</string> <string name="message_muted">Usuário %1$s foi silenciado por %2$s</string>
<string name="message_unmuted">Usuário %1$s saiu do modo mudo por %2$s</string> <string name="message_unmuted">Usuário %1$s saiu do modo silenciado por %2$s</string>
<string name="message_role_add">%1$s foi definido %2$s por %3$s</string> <string name="message_role_add">%1$s foi definido %2$s por %3$s</string>
<string name="message_role_removed">%1$s não é mais %2$s por %3$s</string> <string name="message_role_removed">%1$s não é mais %2$s por %3$s</string>
// TODO:Add proper translation. // TODO:Add proper translation.
...@@ -202,6 +207,7 @@ ...@@ -202,6 +207,7 @@
<string name="action_msg_share">Compartilhar</string> <string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando mensagem</string> <string name="action_title_editing">Editando mensagem</string>
<string name="action_msg_add_reaction">Adicionar reação</string> <string name="action_msg_add_reaction">Adicionar reação</string>
<string name="action_msg_copy_permalink">Copiar permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string> <string name="permission_editing_not_allowed">Edição não permitida</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Ваш сервер</string> <string name="title_sign_in_your_server">Ваш сервер</string>
<string name="title_log_in">Войти</string> <string name="title_log_in">Войти</string>
<string name="title_share_the_app">добавить приложение</string>
<string name="title_register_username">Зарегистрировать имя</string> <string name="title_register_username">Зарегистрировать имя</string>
<string name="title_reset_password">Сброс пароля</string> <string name="title_reset_password">Сброс пароля</string>
<string name="title_sign_up">Зарегистрироваться</string> <string name="title_sign_up">Зарегистрироваться</string>
...@@ -20,13 +21,11 @@ ...@@ -20,13 +21,11 @@
<string name="title_update_profile">Обновить профиль</string> <string name="title_update_profile">Обновить профиль</string>
<string name="title_about">О программе</string> <string name="title_about">О программе</string>
<string name="title_create_channel">Создать новый канал</string> <string name="title_create_channel">Создать новый канал</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Подключиться</string> <string name="action_connect">Подключиться</string>
<string name="action_use_this_username">Использовать это имя</string> <string name="action_use_this_username">Использовать это имя</string>
<string name="action_login_or_sign_up">Нажмите эту кнопку, чтобы войти в систему или создать учетную запись</string>
<string name="action_terms_of_service">Условия использования</string> <string name="action_terms_of_service">Условия использования</string>
<string name="action_privacy_policy">Политика конфиденциальности</string> <string name="action_privacy_policy">Политика конфиденциальности</string>
<string name="action_search">Поиск</string> <string name="action_search">Поиск</string>
...@@ -35,7 +34,6 @@ ...@@ -35,7 +34,6 @@
<string name="action_create_channel">Создать канал</string> <string name="action_create_channel">Создать канал</string>
<string name="action_create">Создать</string> <string name="action_create">Создать</string>
<string name="action_logout">Выйти</string> <string name="action_logout">Выйти</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Файлы</string> <string name="action_files">Файлы</string>
<string name="action_confirm_password">Подтверждение изменения пароля</string> <string name="action_confirm_password">Подтверждение изменения пароля</string>
<string name="action_join_chat">Присоединиться к чату</string> <string name="action_join_chat">Присоединиться к чату</string>
...@@ -54,17 +52,21 @@ ...@@ -54,17 +52,21 @@
<string name="action_create_server">Создать новый сервер</string> <string name="action_create_server">Создать новый сервер</string>
<string name="action_register">Зарегистрировать</string> <string name="action_register">Зарегистрировать</string>
<string name="action_confirm">Подтвердить</string> <string name="action_confirm">Подтвердить</string>
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Персональные</item> <item name="item_preferences">Персональные</item>
<item name="item_password">Изменить пароль</item> <item name="item_password">Изменить пароль</item>
<item name="item_password">О программе</item> <item name="item_password">О программе</item>
<item name="item_share_app">добавить приложение</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Произошла ошибка, повторите попытку.</string> <string name="msg_generic_error">Произошла ошибка, повторите попытку.</string>
<string name="msg_no_data_to_display">Нет данных для отображения</string> <string name="msg_no_data_to_display">Нет данных для отображения</string>
<string name="msg_check_this_out">Проверь это</string>
<string name="msg_share_using">Совместное использование</string>
<string name="msg_profile_update_successfully">Профиль успешно обновлен</string> <string name="msg_profile_update_successfully">Профиль успешно обновлен</string>
<string name="msg_username">имя пользователя</string> <string name="msg_username">имя пользователя</string>
<string name="msg_username_or_email">имя пользователя или e-mail</string> <string name="msg_username_or_email">имя пользователя или e-mail</string>
...@@ -151,6 +153,9 @@ ...@@ -151,6 +153,9 @@
<string name="msg__your_2fa_code">Ваш код 2FA?</string> <string name="msg__your_2fa_code">Ваш код 2FA?</string>
<string name="msg_view_more">больше</string> <string name="msg_view_more">больше</string>
<string name="msg_view_less">меньше</string> <string name="msg_view_less">меньше</string>
<string name="msg_permalink_copied">Ссылка скопирована</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Приватный</string> <string name="msg_private_channel">Приватный</string>
...@@ -199,6 +204,7 @@ ...@@ -199,6 +204,7 @@
<string name="action_msg_share">Поделиться</string> <string name="action_msg_share">Поделиться</string>
<string name="action_title_editing">Редактирование сообщения</string> <string name="action_title_editing">Редактирование сообщения</string>
<string name="action_msg_add_reaction">Отреагировать</string> <string name="action_msg_add_reaction">Отреагировать</string>
<string name="action_msg_copy_permalink">Копировать ссылку</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Редактирование запрещено</string> <string name="permission_editing_not_allowed">Редактирование запрещено</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Sunucunuza Bağlantı Kurun</string> <string name="title_sign_in_your_server">Sunucunuza Bağlantı Kurun</string>
<string name="title_log_in">Giriş Yapın</string> <string name="title_log_in">Giriş Yapın</string>
<string name="title_share_the_app">uygulamayı Paylaş</string>
<string name="title_register_username">Kullanıcı Adıyla Kaydolun</string> <string name="title_register_username">Kullanıcı Adıyla Kaydolun</string>
<string name="title_reset_password">Şifre Sıfırlama</string> <string name="title_reset_password">Şifre Sıfırlama</string>
<string name="title_sign_up">Kayıt Olun</string> <string name="title_sign_up">Kayıt Olun</string>
...@@ -20,13 +21,11 @@ ...@@ -20,13 +21,11 @@
<string name="title_update_profile">Profilinizi Düzenleyin</string> <string name="title_update_profile">Profilinizi Düzenleyin</string>
<string name="title_about">Hakkında</string> <string name="title_about">Hakkında</string>
<string name="title_create_channel">Yeni Kanal Oluştur</string> <string name="title_create_channel">Yeni Kanal Oluştur</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Bağlan</string> <string name="action_connect">Bağlan</string>
<string name="action_use_this_username">Bu kullanıcı adını kullan</string> <string name="action_use_this_username">Bu kullanıcı adını kullan</string>
<string name="action_login_or_sign_up">Giriş yapmak veya yeni hesap oluşturmak için buraya tıklayın.</string>
<string name="action_terms_of_service">Kullanım Şartları</string> <string name="action_terms_of_service">Kullanım Şartları</string>
<string name="action_privacy_policy">Gizlilik Sözleşmesi</string> <string name="action_privacy_policy">Gizlilik Sözleşmesi</string>
<string name="action_search">Ara</string> <string name="action_search">Ara</string>
...@@ -35,7 +34,6 @@ ...@@ -35,7 +34,6 @@
<string name="action_create_channel">Yeni Kanal Oluştur</string> <string name="action_create_channel">Yeni Kanal Oluştur</string>
<string name="action_create">Oluştur</string> <string name="action_create">Oluştur</string>
<string name="action_logout">Çıkış Yap</string> <string name="action_logout">Çıkış Yap</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Dosyalar</string> <string name="action_files">Dosyalar</string>
<string name="action_confirm_password">Şifre Değişikliğini Onaylayın</string> <string name="action_confirm_password">Şifre Değişikliğini Onaylayın</string>
<string name="action_join_chat">Sohbete Bağlan</string> <string name="action_join_chat">Sohbete Bağlan</string>
...@@ -54,17 +52,21 @@ ...@@ -54,17 +52,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Tercihler</item> <item name="item_preferences">Tercihler</item>
<item name="item_password">Şifre Değiştir</item> <item name="item_password">Şifre Değiştir</item>
<item name="item_password">Hakkında</item> <item name="item_password">Hakkında</item>
<item name="item_share_app">uygulamayı Paylaş</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Üzgünüz! bir hata oluştu. Lütfen daha sonra tekrar deneyiniz.</string> <string name="msg_generic_error">Üzgünüz! bir hata oluştu. Lütfen daha sonra tekrar deneyiniz.</string>
<string name="msg_no_data_to_display">Görüntülenecek veri bulunmamaktadır.</string> <string name="msg_no_data_to_display">Görüntülenecek veri bulunmamaktadır.</string>
<string name="msg_check_this_out">bunu kontrol et</string>
<string name="msg_share_using">kullanarak paylaş</string>
<string name="msg_profile_update_successfully">Profil bilgileri başarıyla güncellenmiştir.</string> <string name="msg_profile_update_successfully">Profil bilgileri başarıyla güncellenmiştir.</string>
<string name="msg_username">kullanıcı adı</string> <string name="msg_username">kullanıcı adı</string>
<string name="msg_username_or_email">kullanıcı adı veya eposta</string> <string name="msg_username_or_email">kullanıcı adı veya eposta</string>
...@@ -167,6 +169,10 @@ ...@@ -167,6 +169,10 @@
<string name="msg_delete_description">Bu mesajı silmek istediğinizden emin misiniz</string> <string name="msg_delete_description">Bu mesajı silmek istediğinizden emin misiniz</string>
<string name="msg_view_more">Daha fazla göster</string> <string name="msg_view_more">Daha fazla göster</string>
<string name="msg_view_less">Daha az göster</string> <string name="msg_view_less">Daha az göster</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">İstatistik takibi</string> <string name="msg_analytics_tracking">İstatistik takibi</string>
...@@ -203,6 +209,8 @@ ...@@ -203,6 +209,8 @@
<string name="action_msg_share">Paylaş</string> <string name="action_msg_share">Paylaş</string>
<string name="action_title_editing">Mesaj Düzenleniyor</string> <string name="action_title_editing">Mesaj Düzenleniyor</string>
<string name="action_msg_add_reaction">Tepki Ekle</string> <string name="action_msg_add_reaction">Tepki Ekle</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Düzenlemeye izin verilmiyor</string> <string name="permission_editing_not_allowed">Düzenlemeye izin verilmiyor</string>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Ваш сервер</string> <string name="title_sign_in_your_server">Ваш сервер</string>
<string name="title_log_in">Увійти</string> <string name="title_log_in">Увійти</string>
<string name="title_share_the_app">поділитися прикладом</string>
<string name="title_register_username">Зареєструвати ім\'я</string> <string name="title_register_username">Зареєструвати ім\'я</string>
<string name="title_reset_password">Відновлення паролю</string> <string name="title_reset_password">Відновлення паролю</string>
<string name="title_sign_up">Зареєструватися</string> <string name="title_sign_up">Зареєструватися</string>
...@@ -20,13 +21,11 @@ ...@@ -20,13 +21,11 @@
<string name="title_update_profile">Оновити профіль</string> <string name="title_update_profile">Оновити профіль</string>
<string name="title_about">"Про програму"</string> <string name="title_about">"Про програму"</string>
<string name="title_create_channel">Створити новий канал</string> <string name="title_create_channel">Створити новий канал</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <!-- TODO Add translation --> <string name="title_are_you_sure">Are you sure?</string> <!-- TODO Add translation -->
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Підключитися</string> <string name="action_connect">Підключитися</string>
<string name="action_use_this_username">Використати це ім\'я</string> <string name="action_use_this_username">Використати це ім\'я</string>
<string name="action_login_or_sign_up">Натисніть цю кнопку, щоб увійти до системи або створити аккаунт</string>
<string name="action_terms_of_service">Умови використання</string> <string name="action_terms_of_service">Умови використання</string>
<string name="action_privacy_policy">Політика конфіденційності</string> <string name="action_privacy_policy">Політика конфіденційності</string>
<string name="action_search">Пошук</string> <string name="action_search">Пошук</string>
...@@ -35,7 +34,6 @@ ...@@ -35,7 +34,6 @@
<string name="action_create_channel">Створити канал</string> <string name="action_create_channel">Створити канал</string>
<string name="action_create">Створити</string> <string name="action_create">Створити</string>
<string name="action_logout">Вийти</string> <string name="action_logout">Вийти</string>
<string name="action_stay">Stay</string> <!-- TODO Add translation -->
<string name="action_files">Файли</string> <string name="action_files">Файли</string>
<string name="action_confirm_password">Підтвердження зміни пароля</string> <string name="action_confirm_password">Підтвердження зміни пароля</string>
<string name="action_join_chat">Приєднатися до чату</string> <string name="action_join_chat">Приєднатися до чату</string>
...@@ -54,17 +52,21 @@ ...@@ -54,17 +52,21 @@
<string name="action_create_server">Create a new server</string> <!-- TODO Add translation --> <string name="action_create_server">Create a new server</string> <!-- TODO Add translation -->
<string name="action_register">Register</string> <!-- TODO Add translation --> <string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation --> <string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferences</item> <!-- TODO Add translation --> <item name="item_preferences">Preferences</item> <!-- TODO Add translation -->
<item name="item_password">Change Password</item> <!-- TODO Add translation --> <item name="item_password">Change Password</item> <!-- TODO Add translation -->
<item name="item_password">About</item> <!-- TODO Add translation --> <item name="item_password">About</item> <!-- TODO Add translation -->
<item name="item_share_app">поділитися прикладом</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Сталася помилка, спробуйте ще раз.</string> <string name="msg_generic_error">Сталася помилка, спробуйте ще раз.</string>
<string name="msg_no_data_to_display">Немає даних для відображення</string> <string name="msg_no_data_to_display">Немає даних для відображення</string>
<string name="msg_check_this_out">Перевір це</string>
<string name="msg_share_using">поділитися використанням</string>
<string name="msg_profile_update_successfully">"Профіль оновлено успішно "</string> <string name="msg_profile_update_successfully">"Профіль оновлено успішно "</string>
<string name="msg_username">Ім\'я користувача</string> <string name="msg_username">Ім\'я користувача</string>
<string name="msg_username_or_email">Ім\'я користувача або e-mail</string> <string name="msg_username_or_email">Ім\'я користувача або e-mail</string>
...@@ -152,6 +154,10 @@ ...@@ -152,6 +154,10 @@
<string name="msg_view_more">view more</string> <string name="msg_view_more">view more</string>
<!-- TODO - Add proper translation --> <!-- TODO - Add proper translation -->
<string name="msg_view_less">view less</string> <string name="msg_view_less">view less</string>
<!-- TODO - Add proper translation -->
<string name="msg_permalink_copied">Permalink copied</string>
<!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Приватний</string> <string name="msg_private_channel">Приватний</string>
...@@ -200,6 +206,8 @@ ...@@ -200,6 +206,8 @@
<string name="action_msg_share">Поділитися</string> <string name="action_msg_share">Поділитися</string>
<string name="action_title_editing">Редагування повідомлення</string> <string name="action_title_editing">Редагування повідомлення</string>
<string name="action_msg_add_reaction">Відреагувати</string> <string name="action_msg_add_reaction">Відреагувати</string>
<!-- TODO - Add proper translation -->
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Редагування заборонено</string> <string name="permission_editing_not_allowed">Редагування заборонено</string>
......
...@@ -4,4 +4,5 @@ ...@@ -4,4 +4,5 @@
<string name="server_hint_url" translatable="false">your-company.rocket.chat</string> <string name="server_hint_url" translatable="false">your-company.rocket.chat</string>
<string name="community_server_url" translatable="false">open.rocket.chat</string> <string name="community_server_url" translatable="false">open.rocket.chat</string>
<string name="create_server_url" translatable="false">cloud.rocket.chat/trial</string> <string name="create_server_url" translatable="false">cloud.rocket.chat/trial</string>
<string name="play_store_link" translatable="false">https://play.google.com/store/apps/details?id=chat.rocket.android</string>
</resources> </resources>
\ No newline at end of file
...@@ -15,6 +15,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -15,6 +15,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Sign in to your server</string> <string name="title_sign_in_your_server">Sign in to your server</string>
<string name="title_log_in">Login</string> <string name="title_log_in">Login</string>
<string name="title_share_the_app">Share App</string>
<string name="title_register_username">Register username</string> <string name="title_register_username">Register username</string>
<string name="title_reset_password">Reset password</string> <string name="title_reset_password">Reset password</string>
<string name="title_sign_up">Sign up</string> <string name="title_sign_up">Sign up</string>
...@@ -32,12 +33,11 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -32,12 +33,11 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="title_update_profile">Update profile</string> <string name="title_update_profile">Update profile</string>
<string name="title_about">About</string> <string name="title_about">About</string>
<string name="title_create_channel">Create Channel</string> <string name="title_create_channel">Create Channel</string>
<string name="title_confirmation">Are You Sure you want to logout?</string> <string name="title_are_you_sure">Are you sure?</string>
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Connect</string> <string name="action_connect">Connect</string>
<string name="action_use_this_username">Use this username</string> <string name="action_use_this_username">Use this username</string>
<string name="action_login_or_sign_up">Tap this button to log in or create an account</string>
<string name="action_terms_of_service">Terms of Service</string> <string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string> <string name="action_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
...@@ -46,7 +46,6 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -46,7 +46,6 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="action_create_channel">Create channel</string> <string name="action_create_channel">Create channel</string>
<string name="action_create">Create</string> <string name="action_create">Create</string>
<string name="action_logout">Logout</string> <string name="action_logout">Logout</string>
<string name="action_stay">Stay</string>
<string name="action_files">Files</string> <string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string> <string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string> <string name="action_join_chat">Join Chat</string>
...@@ -65,17 +64,21 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -65,17 +64,21 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="action_create_server">Create a new server</string> <string name="action_create_server">Create a new server</string>
<string name="action_register">Register</string> <string name="action_register">Register</string>
<string name="action_confirm">Confirm</string> <string name="action_confirm">Confirm</string>
<string name="action_delete_account">Delete account</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
<item name="item_preferences">Preferences</item> <item name="item_preferences">Preferences</item>
<item name="item_password">Change Password</item> <item name="item_password">Change Password</item>
<item name="item_password">About</item> <item name="item_password">About</item>
<item name="item_share_app">Share App</item>
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_generic_error">Sorry, an error has occurred, please try again</string> <string name="msg_generic_error">Sorry, an error has occurred, please try again</string>
<string name="msg_no_data_to_display">No data to display</string> <string name="msg_no_data_to_display">No data to display</string>
<string name="msg_check_this_out">Check this out</string>
<string name="msg_share_using">Share using</string>
<string name="msg_profile_update_successfully">Profile update successfully</string> <string name="msg_profile_update_successfully">Profile update successfully</string>
<string name="msg_username">username</string> <string name="msg_username">username</string>
<string name="msg_username_or_email">Username or email</string> <string name="msg_username_or_email">Username or email</string>
...@@ -162,6 +165,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -162,6 +165,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string> <string name="msg_continue_with_wordpress">Continue with <b>WordPress</b></string>
<string name="msg_two_factor_authentication">Two-factor Authentication</string> <string name="msg_two_factor_authentication">Two-factor Authentication</string>
<string name="msg__your_2fa_code">What’s your 2FA code?</string> <string name="msg__your_2fa_code">What’s your 2FA code?</string>
<string name="msg_permalink_copied">Permalink copied</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Private</string> <string name="msg_private_channel">Private</string>
...@@ -179,6 +183,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -179,6 +183,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_delete_description">Are you sure you want to delete this message</string>
<string name="msg_view_more">view more</string> <string name="msg_view_more">view more</string>
<string name="msg_view_less">view less</string> <string name="msg_view_less">view less</string>
<string name="msg_muted_on_this_channel">You are muted on this channel</string>
<!-- Preferences messages --> <!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <string name="msg_analytics_tracking">Analytics tracking</string>
...@@ -215,6 +220,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin ...@@ -215,6 +220,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="action_msg_share">Share</string> <string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string> <string name="action_title_editing">Editing Message</string>
<string name="action_msg_add_reaction">Add reaction</string> <string name="action_msg_add_reaction">Add reaction</string>
<string name="action_msg_copy_permalink">Copy permalink</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string> <string name="permission_editing_not_allowed">Editing is not allowed</string>
......
...@@ -32,20 +32,46 @@ suspend fun Bitmap.compressImageAndGetInputStream(mimeType: String): InputStream ...@@ -32,20 +32,46 @@ suspend fun Bitmap.compressImageAndGetInputStream(mimeType: String): InputStream
return inputStream return inputStream
} }
/**
* Returns a [ByteArray] of a [Bitmap].
*
* @param mimeType The MIME type of the [Bitmap].
* @param quality The quality of the [Bitmap] for the resulting [ByteArray].
* @param maxFileSizeAllowed The max file size allowed by the server. Note: The [quality] will be
* decreased minus 10 until the [ByteArray] size fits the [maxFileSizeAllowed] value.
* @return A [ByteArray] of a [Bitmap]
*/
suspend fun Bitmap.getByteArray(
mimeType: String,
quality: Int,
maxFileSizeAllowed: Int
): ByteArray {
lateinit var byteArray: ByteArray
compressImageAndGetByteArray(mimeType, quality)?.let {
if (it.size > maxFileSizeAllowed && maxFileSizeAllowed !in -1..0) {
getByteArray(mimeType, quality - 10, maxFileSizeAllowed)
} else {
byteArray = it
}
}
return byteArray
}
/** /**
* Compress a [Bitmap] image. * Compress a [Bitmap] image.
* *
* @param mimeType The MimeType of what the compressed image should be. * @param mimeType The MimeType of what the compressed image should be.
* @return An [ByteArray] of a compressed image, otherwise null if the compression couldn't be done. * @return An [ByteArray] of a compressed image, otherwise null if the compression couldn't be done.
*/ */
suspend fun Bitmap.compressImageAndGetByteArray(mimeType: String): ByteArray? { suspend fun Bitmap.compressImageAndGetByteArray(mimeType: String, quality: Int = 100): ByteArray? {
var byteArray: ByteArray? = null var byteArray: ByteArray? = null
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
// TODO: Add an option the the app to the user be able to select the quality of the compressed image
val isCompressed = val isCompressed =
this.compress(mimeType.getCompressFormat(), 70, byteArrayOutputStream) this.compress(mimeType.getCompressFormat(), quality, byteArrayOutputStream)
if (isCompressed) { if (isCompressed) {
byteArray = byteArrayOutputStream.toByteArray() byteArray = byteArrayOutputStream.toByteArray()
} }
......
package chat.rocket.android.util.extension
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
@Throws(NoSuchAlgorithmException::class)
fun String.gethash(): ByteArray {
val digest = MessageDigest.getInstance("SHA-256")
digest.reset()
return digest.digest(this.toByteArray())
}
fun ByteArray.toHex(): String = String.format("%0" + this.size * 2 + "X", BigInteger(1, this))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment