mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-09 22:04:24 +01:00
Merge pull request #4422 from nextcloud/feature/4378/addTemporaryMessagesWhileSending
Feature/4378/add temporary messages while sending
This commit is contained in:
commit
88bb5d506a
@ -0,0 +1,749 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 13,
|
||||
"identityHash": "a521f027909f69f4c7d1855f84a2e67f",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "User",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushConfigurationState",
|
||||
"columnName": "pushConfigurationState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "capabilities",
|
||||
"columnName": "capabilities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverVersion",
|
||||
"columnName": "serverVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientCertificate",
|
||||
"columnName": "clientCertificate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalSignalingServer",
|
||||
"columnName": "externalSignalingServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "current",
|
||||
"columnName": "current",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForDeletion",
|
||||
"columnName": "scheduledForDeletion",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "ArbitraryStorage",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "accountIdentifier",
|
||||
"columnName": "accountIdentifier",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "storageObject",
|
||||
"columnName": "object",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"accountIdentifier",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Conversations",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "internalId",
|
||||
"columnName": "internalId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actorId",
|
||||
"columnName": "actorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actorType",
|
||||
"columnName": "actorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatarVersion",
|
||||
"columnName": "avatarVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "callFlag",
|
||||
"columnName": "callFlag",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "callRecording",
|
||||
"columnName": "callRecording",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "callStartTime",
|
||||
"columnName": "callStartTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canDeleteConversation",
|
||||
"columnName": "canDeleteConversation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canLeaveConversation",
|
||||
"columnName": "canLeaveConversation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canStartCall",
|
||||
"columnName": "canStartCall",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasCall",
|
||||
"columnName": "hasCall",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasPassword",
|
||||
"columnName": "hasPassword",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasCustomAvatar",
|
||||
"columnName": "isCustomAvatar",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favorite",
|
||||
"columnName": "isFavorite",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastActivity",
|
||||
"columnName": "lastActivity",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastCommonReadMessage",
|
||||
"columnName": "lastCommonReadMessage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastMessage",
|
||||
"columnName": "lastMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastPing",
|
||||
"columnName": "lastPing",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastReadMessage",
|
||||
"columnName": "lastReadMessage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lobbyState",
|
||||
"columnName": "lobbyState",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lobbyTimer",
|
||||
"columnName": "lobbyTimer",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageExpiration",
|
||||
"columnName": "messageExpiration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationCalls",
|
||||
"columnName": "notificationCalls",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationLevel",
|
||||
"columnName": "notificationLevel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "objectType",
|
||||
"columnName": "objectType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "participantType",
|
||||
"columnName": "participantType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "permissions",
|
||||
"columnName": "permissions",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conversationReadOnlyState",
|
||||
"columnName": "readOnly",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recordingConsentRequired",
|
||||
"columnName": "recordingConsent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteServer",
|
||||
"columnName": "remoteServer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteToken",
|
||||
"columnName": "remoteToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sessionId",
|
||||
"columnName": "sessionId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusClearAt",
|
||||
"columnName": "statusClearAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusIcon",
|
||||
"columnName": "statusIcon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "statusMessage",
|
||||
"columnName": "statusMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unreadMention",
|
||||
"columnName": "unreadMention",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unreadMentionDirect",
|
||||
"columnName": "unreadMentionDirect",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unreadMessages",
|
||||
"columnName": "unreadMessages",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasArchived",
|
||||
"columnName": "hasArchived",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"internalId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Conversations_accountId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"accountId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "User",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"accountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ChatMessages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "internalId",
|
||||
"columnName": "internalId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "internalConversationId",
|
||||
"columnName": "internalConversationId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actorDisplayName",
|
||||
"columnName": "actorDisplayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actorId",
|
||||
"columnName": "actorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actorType",
|
||||
"columnName": "actorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deleted",
|
||||
"columnName": "deleted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "expirationTimestamp",
|
||||
"columnName": "expirationTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "replyable",
|
||||
"columnName": "isReplyable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTemporary",
|
||||
"columnName": "isTemporary",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorDisplayName",
|
||||
"columnName": "lastEditActorDisplayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorId",
|
||||
"columnName": "lastEditActorId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditActorType",
|
||||
"columnName": "lastEditActorType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEditTimestamp",
|
||||
"columnName": "lastEditTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "renderMarkdown",
|
||||
"columnName": "markdown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageParameters",
|
||||
"columnName": "messageParameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageType",
|
||||
"columnName": "messageType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentMessageId",
|
||||
"columnName": "parent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactions",
|
||||
"columnName": "reactions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reactionsSelf",
|
||||
"columnName": "reactionsSelf",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "referenceId",
|
||||
"columnName": "referenceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendingFailed",
|
||||
"columnName": "sendingFailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "silent",
|
||||
"columnName": "silent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "systemMessageType",
|
||||
"columnName": "systemMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"internalId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ChatMessages_internalId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"internalId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_ChatMessages_internalConversationId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"internalConversationId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Conversations",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"internalConversationId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"internalId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ChatBlocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "internalConversationId",
|
||||
"columnName": "internalConversationId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "token",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "oldestMessageId",
|
||||
"columnName": "oldestMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "newestMessageId",
|
||||
"columnName": "newestMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasHistory",
|
||||
"columnName": "hasHistory",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ChatBlocks_internalConversationId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"internalConversationId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "Conversations",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"internalConversationId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"internalId"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, 'a521f027909f69f4c7d1855f84a2e67f')"
|
||||
]
|
||||
}
|
||||
}
|
@ -218,7 +218,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
|
||||
)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -23,6 +23,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.network.NetworkMonitor
|
||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
@ -58,8 +59,12 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
lateinit var commonMessageInterface: CommonMessageInterface
|
||||
|
||||
@Suppress("Detekt.LongMethod")
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
@ -68,6 +73,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
layoutParams.isWrapBefore = false
|
||||
var textSize = context.resources.getDimension(R.dimen.chat_text_size)
|
||||
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
|
||||
|
||||
var processedMessageText = messageUtils.enrichChatMessageText(
|
||||
binding.messageText.context,
|
||||
message,
|
||||
@ -114,7 +120,27 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
|
||||
setReadStatus(message.readStatus)
|
||||
binding.checkMark.visibility = View.INVISIBLE
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
|
||||
if (message.sendingFailed) {
|
||||
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
|
||||
} else if (message.isTemporary) {
|
||||
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))
|
||||
} else if (message.readStatus == ReadStatus.READ) {
|
||||
updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read))
|
||||
} else if (message.readStatus == ReadStatus.SENT) {
|
||||
updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent))
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (message.isTemporary && !networkMonitor.isOnline.first()) {
|
||||
updateStatus(
|
||||
R.drawable.ic_signal_wifi_off_white_24dp,
|
||||
context.resources?.getString(R.string.nc_message_offline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
|
||||
|
||||
@ -129,27 +155,16 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
)
|
||||
}
|
||||
|
||||
private fun setReadStatus(readStatus: Enum<ReadStatus>) {
|
||||
val readStatusDrawableInt = when (readStatus) {
|
||||
ReadStatus.READ -> R.drawable.ic_check_all
|
||||
ReadStatus.SENT -> R.drawable.ic_check
|
||||
else -> null
|
||||
}
|
||||
|
||||
val readStatusContentDescriptionString = when (readStatus) {
|
||||
ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read)
|
||||
ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
readStatusDrawableInt?.let { drawableInt ->
|
||||
private fun updateStatus(readStatusDrawableInt: Int, description: String?) {
|
||||
binding.sendingProgress.visibility = View.GONE
|
||||
binding.checkMark.visibility = View.VISIBLE
|
||||
readStatusDrawableInt.let { drawableInt ->
|
||||
ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let {
|
||||
binding.checkMark.setImageDrawable(it)
|
||||
viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark)
|
||||
}
|
||||
}
|
||||
|
||||
binding.checkMark.contentDescription = readStatusContentDescriptionString
|
||||
binding.checkMark.contentDescription = description
|
||||
}
|
||||
|
||||
private fun longClickOnReaction(chatMessage: ChatMessage) {
|
||||
@ -180,7 +195,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
).first()
|
||||
}
|
||||
|
||||
parentChatMessage!!.activeUser = message.activeUser
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
@ -207,7 +222,6 @@ class OutcomingTextMessageViewHolder(itemView: View) :
|
||||
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
val chatActivity = commonMessageInterface as ChatActivity
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -68,9 +68,6 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
|
||||
} else if (holder instanceof SystemMessageViewHolder holderInstance) {
|
||||
holderInstance.assignSystemMessageInterface(chatActivity);
|
||||
|
||||
} else if (holder instanceof TemporaryMessageViewHolder holderInstance) {
|
||||
holderInstance.assignTemporaryMessageInterface(chatActivity);
|
||||
|
||||
} else if (holder instanceof IncomingDeckCardViewHolder holderInstance) {
|
||||
holderInstance.assignCommonMessageInterface(chatActivity);
|
||||
} else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) {
|
||||
|
@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
interface TemporaryMessageInterface {
|
||||
fun editTemporaryMessage(id: Int, newMessage: String)
|
||||
fun deleteTemporaryMessage(id: Int)
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemTemporaryMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessagesListAdapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class TemporaryMessageViewHolder(outgoingView: View, payload: Any) :
|
||||
MessagesListAdapter.OutcomingMessageViewHolder<ChatMessage>(outgoingView) {
|
||||
|
||||
private val binding: ItemTemporaryMessageBinding = ItemTemporaryMessageBinding.bind(outgoingView)
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
lateinit var temporaryMessageInterface: TemporaryMessageInterface
|
||||
var isEditing = false
|
||||
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
viewThemeUtils.platform.colorImageView(binding.tempMsgEdit, ColorRole.PRIMARY)
|
||||
viewThemeUtils.platform.colorImageView(binding.tempMsgDelete, ColorRole.PRIMARY)
|
||||
|
||||
binding.tempMsgEdit.setOnClickListener {
|
||||
isEditing = !isEditing
|
||||
if (isEditing) {
|
||||
binding.tempMsgEdit.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_check,
|
||||
null
|
||||
)
|
||||
)
|
||||
binding.messageEdit.visibility = View.VISIBLE
|
||||
binding.messageEdit.requestFocus()
|
||||
ViewCompat.getWindowInsetsController(binding.root)?.show(WindowInsetsCompat.Type.ime())
|
||||
binding.messageEdit.setText(binding.messageText.text)
|
||||
binding.messageText.visibility = View.GONE
|
||||
} else {
|
||||
binding.tempMsgEdit.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_edit,
|
||||
null
|
||||
)
|
||||
)
|
||||
binding.messageEdit.visibility = View.GONE
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
val newMessage = binding.messageEdit.text.toString()
|
||||
message.message = newMessage
|
||||
temporaryMessageInterface.editTemporaryMessage(message.tempMessageId, newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
binding.tempMsgDelete.setOnClickListener {
|
||||
temporaryMessageInterface.deleteTemporaryMessage(message.tempMessageId)
|
||||
}
|
||||
|
||||
// parent message handling
|
||||
if (message.parentMessageId != null && message.parentMessageId!! > 0) {
|
||||
processParentMessage(message)
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val bgBubbleColor = bubble.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
|
||||
val layout = R.drawable.shape_outcoming_message
|
||||
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
||||
bgBubbleColor,
|
||||
ResourcesCompat.getColor(bubble.resources, R.color.transparent, null),
|
||||
bgBubbleColor,
|
||||
layout
|
||||
)
|
||||
ViewCompat.setBackground(bubble, bubbleDrawable)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = temporaryMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
|
||||
parentChatMessage.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
val placeholder = ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_mimetype_image,
|
||||
null
|
||||
)
|
||||
binding.messageQuote.quotedMessageImage.setImageDrawable(placeholder)
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
val chatActivity = temporaryMessageInterface as ChatActivity
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assignTemporaryMessageInterface(temporaryMessageInterface: TemporaryMessageInterface) {
|
||||
this.temporaryMessageInterface = temporaryMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun viewAttached() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun viewRecycled() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = TemporaryMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
@ -344,18 +344,14 @@ public interface NcApi {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<GenericOverall> sendChatMessage(@Header("Authorization") String authorization,
|
||||
Observable<ChatOverallSingleMessage> sendChatMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("message") CharSequence message,
|
||||
@Field("actorDisplayName") String actorDisplayName,
|
||||
@Field("replyTo") Integer replyTo,
|
||||
@Field("silent") Boolean sendWithoutNotification);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
Observable<ChatOverallSingleMessage> editChatMessage(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Field("message") String message);
|
||||
@Field("silent") Boolean sendWithoutNotification,
|
||||
@Field("referenceId") String referenceId
|
||||
);
|
||||
|
||||
@GET
|
||||
Observable<Response<ChatShareOverall>> getSharedItems(
|
||||
|
@ -8,6 +8,7 @@
|
||||
package com.nextcloud.talk.api
|
||||
|
||||
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import com.nextcloud.talk.models.json.conversations.RoomOverall
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
import com.nextcloud.talk.models.json.participants.AddParticipantOverall
|
||||
@ -30,6 +31,7 @@ import retrofit2.http.Query
|
||||
import retrofit2.http.QueryMap
|
||||
import retrofit2.http.Url
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface NcApiCoroutines {
|
||||
@GET
|
||||
@JvmSuppressWildcards
|
||||
@ -122,6 +124,27 @@ interface NcApiCoroutines {
|
||||
@DELETE
|
||||
suspend fun unarchiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun sendChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String,
|
||||
@Field("actorDisplayName") actorDisplayName: String,
|
||||
@Field("replyTo") replyTo: Int,
|
||||
@Field("silent") sendWithoutNotification: Boolean,
|
||||
@Field("referenceId") referenceId: String
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT
|
||||
suspend fun editChatMessage(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@Field("message") message: String
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
suspend fun banActor(
|
||||
|
@ -111,8 +111,6 @@ import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.SystemMessageInterface
|
||||
import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
|
||||
import com.nextcloud.talk.adapters.messages.TemporaryMessageInterface
|
||||
import com.nextcloud.talk.adapters.messages.TemporaryMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
@ -154,6 +152,7 @@ import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
|
||||
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
|
||||
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
|
||||
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
|
||||
import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog
|
||||
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
|
||||
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
@ -208,7 +207,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
@ -231,8 +229,7 @@ class ChatActivity :
|
||||
CommonMessageInterface,
|
||||
PreviewMessageInterface,
|
||||
SystemMessageInterface,
|
||||
CallStartedMessageInterface,
|
||||
TemporaryMessageInterface {
|
||||
CallStartedMessageInterface {
|
||||
|
||||
var active = false
|
||||
|
||||
@ -319,7 +316,6 @@ class ChatActivity :
|
||||
var startCallFromNotification: Boolean = false
|
||||
var startCallFromRoomSwitch: Boolean = false
|
||||
|
||||
// lateinit var roomId: String
|
||||
var voiceOnly: Boolean = true
|
||||
private lateinit var path: String
|
||||
|
||||
@ -452,6 +448,7 @@ class ChatActivity :
|
||||
chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
|
||||
|
||||
messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
|
||||
messageInputViewModel.setData(chatViewModel.getChatRepository())
|
||||
|
||||
this.lifecycleScope.launch {
|
||||
delay(DELAY_TO_SHOW_PROGRESS_BAR)
|
||||
@ -524,7 +521,6 @@ class ChatActivity :
|
||||
private fun handleIntent(intent: Intent) {
|
||||
val extras: Bundle? = intent.extras
|
||||
|
||||
// roomId = extras?.getString(KEY_ROOM_ID).orEmpty()
|
||||
roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
|
||||
|
||||
sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
|
||||
@ -583,35 +579,6 @@ class ChatActivity :
|
||||
private fun initObservers() {
|
||||
Log.d(TAG, "initObservers Called")
|
||||
|
||||
messageInputViewModel.messageQueueFlow.observe(this) { list ->
|
||||
list.forEachIndexed { _, qMsg ->
|
||||
val temporaryChatMessage = ChatMessage()
|
||||
temporaryChatMessage.jsonMessageId = TEMPORARY_MESSAGE_ID_INT
|
||||
temporaryChatMessage.actorId = "-3"
|
||||
temporaryChatMessage.timestamp = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
|
||||
temporaryChatMessage.message = qMsg.message.toString()
|
||||
temporaryChatMessage.tempMessageId = qMsg.id
|
||||
temporaryChatMessage.isTempMessage = true
|
||||
temporaryChatMessage.parentMessageId = qMsg.replyTo!!.toLong()
|
||||
val pos = adapter?.getMessagePositionById(qMsg.replyTo.toString())
|
||||
adapter?.addToStart(temporaryChatMessage, true)
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
messageInputViewModel.messageQueueSizeFlow.observe(this) { size ->
|
||||
if (size == 0) {
|
||||
var i = 0
|
||||
var pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING)
|
||||
while (pos != null && pos > -1) {
|
||||
adapter?.items?.removeAt(pos)
|
||||
i++
|
||||
pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING)
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
this.lifecycleScope.launch {
|
||||
chatViewModel.getConversationFlow
|
||||
.onEach { conversationModel ->
|
||||
@ -719,7 +686,6 @@ class ChatActivity :
|
||||
withCredentials = credentials!!,
|
||||
withUrl = urlForChatting
|
||||
)
|
||||
messageInputViewModel.getTempMessagesFromMessageQueue(currentConversation!!.internalId)
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
@ -744,7 +710,6 @@ class ChatActivity :
|
||||
|
||||
sessionIdAfterRoomJoined = currentConversation!!.sessionId
|
||||
ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation!!.sessionId
|
||||
// ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = currentConversation!!.roomId
|
||||
ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = currentConversation!!.token
|
||||
ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
|
||||
|
||||
@ -813,18 +778,7 @@ class ChatActivity :
|
||||
}
|
||||
|
||||
is MessageInputViewModel.SendChatMessageErrorState -> {
|
||||
if (state.e is HttpException) {
|
||||
val code = state.e.code()
|
||||
if (code.toString().startsWith("2")) {
|
||||
myFirstMessage = state.message
|
||||
|
||||
if (binding.unreadMessagesPopup.isShown) {
|
||||
binding.unreadMessagesPopup.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.messagesListView.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
binding.messagesListView.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
@ -861,7 +815,6 @@ class ChatActivity :
|
||||
is ChatViewModel.CreateRoomSuccessState -> {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_ROOM_TOKEN, state.roomOverall.ocs!!.data!!.token)
|
||||
// bundle.putString(KEY_ROOM_ID, state.roomOverall.ocs!!.data!!.roomId)
|
||||
|
||||
leaveRoom {
|
||||
val chatIntent = Intent(context, ChatActivity::class.java)
|
||||
@ -937,6 +890,14 @@ class ChatActivity :
|
||||
.collect()
|
||||
}
|
||||
|
||||
this.lifecycleScope.launch {
|
||||
chatViewModel.getRemoveMessageFlow
|
||||
.onEach {
|
||||
removeMessageById(it.id)
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
this.lifecycleScope.launch {
|
||||
chatViewModel.getUpdateMessageFlow
|
||||
.onEach {
|
||||
@ -1081,8 +1042,10 @@ class ChatActivity :
|
||||
is ChatViewModel.OutOfOfficeUIState.Error -> {
|
||||
Log.e(TAG, "Error fetching/ no user absence data", uiState.exception)
|
||||
}
|
||||
|
||||
ChatViewModel.OutOfOfficeUIState.None -> {
|
||||
}
|
||||
|
||||
is ChatViewModel.OutOfOfficeUIState.Success -> {
|
||||
binding.outOfOfficeContainer.visibility = View.VISIBLE
|
||||
|
||||
@ -1171,9 +1134,25 @@ class ChatActivity :
|
||||
}
|
||||
|
||||
private fun removeUnreadMessagesMarker() {
|
||||
val index = adapter?.getMessagePositionById(UNREAD_MESSAGES_MARKER_ID.toString())
|
||||
if (index != null && index != -1) {
|
||||
adapter?.items?.removeAt(index)
|
||||
removeMessageById(UNREAD_MESSAGES_MARKER_ID.toString())
|
||||
}
|
||||
|
||||
// do not use adapter.deleteById() as it seems to contain a bug! Use this method instead!
|
||||
@Suppress("MagicNumber")
|
||||
private fun removeMessageById(idToDelete: String) {
|
||||
val indexToDelete = adapter?.getMessagePositionById(idToDelete)
|
||||
if (indexToDelete != null && indexToDelete != UNREAD_MESSAGES_MARKER_ID) {
|
||||
// If user sent a message as a first message in todays chat, the temp message will be deleted when
|
||||
// messages are retrieved from server, but also the date has to be deleted as it will be added again
|
||||
// when the chat messages are added from server. Otherwise date "Today" would be shown twice.
|
||||
if (indexToDelete == 0 && (adapter?.items?.get(1))?.item is Date) {
|
||||
adapter?.items?.removeAt(0)
|
||||
adapter?.items?.removeAt(0)
|
||||
adapter?.notifyItemRangeRemoved(indexToDelete, 1)
|
||||
} else {
|
||||
adapter?.items?.removeAt(indexToDelete)
|
||||
adapter?.notifyItemRemoved(indexToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1190,7 +1169,7 @@ class ChatActivity :
|
||||
|
||||
cancelNotificationsForCurrentConversation()
|
||||
|
||||
chatViewModel.getRoom(conversationUser!!, roomToken)
|
||||
chatViewModel.getRoom(roomToken)
|
||||
|
||||
actionBar?.show()
|
||||
|
||||
@ -1238,18 +1217,18 @@ class ChatActivity :
|
||||
viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
|
||||
}
|
||||
|
||||
private fun getLastAdapterId(): Int {
|
||||
var lastId = 0
|
||||
if (adapter?.items?.size != 0) {
|
||||
val item = adapter?.items?.get(0)?.item
|
||||
if (item != null) {
|
||||
lastId = (item as ChatMessage).jsonMessageId
|
||||
} else {
|
||||
lastId = 0
|
||||
}
|
||||
}
|
||||
return lastId
|
||||
}
|
||||
// private fun getLastAdapterId(): Int {
|
||||
// var lastId = 0
|
||||
// if (adapter?.items?.size != 0) {
|
||||
// val item = adapter?.items?.get(0)?.item
|
||||
// if (item != null) {
|
||||
// lastId = (item as ChatMessage).jsonMessageId
|
||||
// } else {
|
||||
// lastId = 0
|
||||
// }
|
||||
// }
|
||||
// return lastId
|
||||
// }
|
||||
|
||||
private fun setupActionBar() {
|
||||
setSupportActionBar(binding.chatToolbar)
|
||||
@ -1369,17 +1348,6 @@ class ChatActivity :
|
||||
R.layout.item_custom_outcoming_preview_message
|
||||
)
|
||||
|
||||
messageHolders.registerContentType(
|
||||
CONTENT_TYPE_TEMP,
|
||||
TemporaryMessageViewHolder::class.java,
|
||||
payload,
|
||||
R.layout.item_temporary_message,
|
||||
TemporaryMessageViewHolder::class.java,
|
||||
payload,
|
||||
R.layout.item_temporary_message,
|
||||
this
|
||||
)
|
||||
|
||||
messageHolders.registerContentType(
|
||||
CONTENT_TYPE_SYSTEM_MESSAGE,
|
||||
SystemMessageViewHolder::class.java,
|
||||
@ -1658,7 +1626,7 @@ class ChatActivity :
|
||||
}
|
||||
getRoomInfoTimerHandler?.postDelayed(
|
||||
{
|
||||
chatViewModel.getRoom(conversationUser!!, roomToken)
|
||||
chatViewModel.getRoom(roomToken)
|
||||
},
|
||||
delayForRecursiveCall
|
||||
)
|
||||
@ -2727,7 +2695,6 @@ class ChatActivity :
|
||||
) {
|
||||
sessionIdAfterRoomJoined = ApplicationWideCurrentRoomHolder.getInstance().session
|
||||
|
||||
// ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
|
||||
ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
|
||||
ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
|
||||
}
|
||||
@ -2918,7 +2885,6 @@ class ChatActivity :
|
||||
) {
|
||||
if (message.item is ChatMessage) {
|
||||
val chatMessage = message.item as ChatMessage
|
||||
|
||||
if (chatMessage.jsonMessageId <= xChatLastCommonRead) {
|
||||
chatMessage.readStatus = ReadStatus.READ
|
||||
} else {
|
||||
@ -2968,7 +2934,19 @@ class ChatActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun isScrolledToBottom() = layoutManager?.findFirstVisibleItemPosition() == 0
|
||||
private fun isScrolledToBottom(): Boolean {
|
||||
val position = layoutManager?.findFirstVisibleItemPosition()
|
||||
if (position == -1) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"FirstVisibleItemPosition was -1 but true is returned for isScrolledToBottom(). This can " +
|
||||
"happen when the UI is not yet ready"
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return layoutManager?.findFirstVisibleItemPosition() == 0
|
||||
}
|
||||
|
||||
private fun setUnreadMessageMarker(chatMessageList: List<ChatMessage>) {
|
||||
if (chatMessageList.isNotEmpty()) {
|
||||
@ -3354,7 +3332,6 @@ class ChatActivity :
|
||||
currentConversation?.let {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
// bundle.putString(KEY_ROOM_ID, roomId)
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
|
||||
bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl!!)
|
||||
bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
|
||||
@ -3423,9 +3400,14 @@ class ChatActivity :
|
||||
|
||||
private fun openMessageActionsDialog(iMessage: IMessage?) {
|
||||
val message = iMessage as ChatMessage
|
||||
if (hasVisibleItems(message) &&
|
||||
!isSystemMessage(message) &&
|
||||
message.id != "-3"
|
||||
|
||||
if (message.isTemporary) {
|
||||
TempMessageActionsDialog(
|
||||
this,
|
||||
message
|
||||
).show()
|
||||
} else if (hasVisibleItems(message) &&
|
||||
!isSystemMessage(message)
|
||||
) {
|
||||
MessageActionsDialog(
|
||||
this,
|
||||
@ -3849,7 +3831,6 @@ class ChatActivity :
|
||||
CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
|
||||
CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == UNREAD_MESSAGES_MARKER_ID.toString()
|
||||
CONTENT_TYPE_CALL_STARTED -> message.id == "-2"
|
||||
CONTENT_TYPE_TEMP -> message.id == "-3"
|
||||
CONTENT_TYPE_DECK_CARD -> message.isDeckCard()
|
||||
|
||||
else -> false
|
||||
@ -3996,30 +3977,6 @@ class ChatActivity :
|
||||
startACall(false, false)
|
||||
}
|
||||
|
||||
override fun editTemporaryMessage(id: Int, newMessage: String) {
|
||||
messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage)
|
||||
adapter?.notifyDataSetChanged() // TODO optimize this
|
||||
}
|
||||
|
||||
override fun deleteTemporaryMessage(id: Int) {
|
||||
messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id)
|
||||
var i = 0
|
||||
val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1)
|
||||
for (item in adapter?.items!!) {
|
||||
if (i > max!! && max < 1) break
|
||||
if (item.item is ChatMessage &&
|
||||
(item.item as ChatMessage).isTempMessage &&
|
||||
(item.item as ChatMessage).tempMessageId == id
|
||||
) {
|
||||
val index = adapter?.items!!.indexOf(item)
|
||||
adapter?.items!!.removeAt(index)
|
||||
adapter?.notifyItemRemoved(index)
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private fun logConversationInfos(methodName: String) {
|
||||
Log.d(TAG, " |-----------------------------------------------")
|
||||
Log.d(TAG, " | method: $methodName")
|
||||
@ -4068,9 +4025,7 @@ class ChatActivity :
|
||||
private const val CONTENT_TYPE_POLL: Byte = 6
|
||||
private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7
|
||||
private const val CONTENT_TYPE_DECK_CARD: Byte = 8
|
||||
private const val CONTENT_TYPE_TEMP: Byte = 9
|
||||
private const val UNREAD_MESSAGES_MARKER_ID = -1
|
||||
private const val CALL_STARTED_ID = -2
|
||||
private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
|
||||
private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000
|
||||
private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
|
||||
@ -4108,8 +4063,6 @@ class ChatActivity :
|
||||
private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
|
||||
private const val DELAY_TO_SHOW_PROGRESS_BAR = 1000L
|
||||
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
|
||||
private const val TEMPORARY_MESSAGE_ID_INT: Int = -3
|
||||
private const val TEMPORARY_MESSAGE_ID_STRING: String = "-3"
|
||||
private const val ROOM_TYPE_ONE_TO_ONE = "1"
|
||||
private const val ACTOR_TYPE = "users"
|
||||
const val CONVERSATION_INTERNAL_ID = "CONVERSATION_INTERNAL_ID"
|
||||
|
@ -158,7 +158,6 @@ class MessageInputFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
chatActivity.messageInputViewModel.restoreMessageQueue(conversationInternalId)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@ -199,19 +198,20 @@ class MessageInputFragment : Fragment() {
|
||||
wasOnline = !binding.fragmentConnectionLost.isShown
|
||||
val connectionGained = (!wasOnline && isOnline)
|
||||
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
|
||||
handleMessageQueue(isOnline)
|
||||
if (connectionGained) {
|
||||
chatActivity.messageInputViewModel.sendTempMessages(
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser!!.baseUrl!!,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
)
|
||||
}
|
||||
handleUI(isOnline, connectionGained)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
chatActivity.messageInputViewModel.messageQueueSizeFlow.observe(viewLifecycleOwner) { size ->
|
||||
if (size > 0) {
|
||||
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_queued, size)
|
||||
} else {
|
||||
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued)
|
||||
}
|
||||
}
|
||||
|
||||
chatActivity.messageInputViewModel.callStartedFlow.observe(viewLifecycleOwner) {
|
||||
val (message, show) = it
|
||||
if (show) {
|
||||
@ -292,23 +292,6 @@ class MessageInputFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessageQueue(isOnline: Boolean) {
|
||||
if (isOnline) {
|
||||
chatActivity.messageInputViewModel.switchToMessageQueue(false)
|
||||
chatActivity.messageInputViewModel.sendAndEmptyMessageQueue(
|
||||
conversationInternalId,
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser!!.baseUrl!!,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
)
|
||||
} else {
|
||||
chatActivity.messageInputViewModel.switchToMessageQueue(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreState() {
|
||||
if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) {
|
||||
requireContext().getSharedPreferences(chatActivity.localClassName, AppCompatActivity.MODE_PRIVATE).apply {
|
||||
@ -868,7 +851,7 @@ class MessageInputFragment : Fragment() {
|
||||
.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int? ?: 0
|
||||
|
||||
sendMessage(
|
||||
editable,
|
||||
editable.toString(),
|
||||
replyMessageId,
|
||||
sendWithoutNotification
|
||||
)
|
||||
@ -876,9 +859,8 @@ class MessageInputFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
|
||||
private fun sendMessage(message: String, replyTo: Int?, sendWithoutNotification: Boolean) {
|
||||
chatActivity.messageInputViewModel.sendChatMessage(
|
||||
conversationInternalId,
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
@ -917,16 +899,23 @@ class MessageInputFragment : Fragment() {
|
||||
// FIXME Fix API checking with guests?
|
||||
val apiVersion: Int = ApiUtils.getChatApiVersion(chatActivity.spreedCapabilities, intArrayOf(1))
|
||||
|
||||
chatActivity.messageInputViewModel.editChatMessage(
|
||||
chatActivity.credentials!!,
|
||||
ApiUtils.getUrlForChatMessage(
|
||||
apiVersion,
|
||||
chatActivity.conversationUser!!.baseUrl!!,
|
||||
chatActivity.roomToken,
|
||||
message.id
|
||||
),
|
||||
editedMessageText
|
||||
)
|
||||
if (message.isTemporary) {
|
||||
chatActivity.messageInputViewModel.editTempChatMessage(
|
||||
message,
|
||||
editedMessageText
|
||||
)
|
||||
} else {
|
||||
chatActivity.messageInputViewModel.editChatMessage(
|
||||
chatActivity.credentials!!,
|
||||
ApiUtils.getUrlForChatMessage(
|
||||
apiVersion,
|
||||
chatActivity.conversationUser!!.baseUrl!!,
|
||||
chatActivity.roomToken,
|
||||
message.id
|
||||
),
|
||||
editedMessageText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEditUI(message: ChatMessage) {
|
||||
|
@ -11,6 +11,7 @@ import android.os.Bundle
|
||||
import com.nextcloud.talk.chat.data.io.LifecycleAwareManager
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@ -41,6 +42,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
|
||||
*/
|
||||
val generalUIFlow: Flow<String>
|
||||
|
||||
val removeMessageFlow: Flow<ChatMessage>
|
||||
|
||||
fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String)
|
||||
|
||||
fun loadInitialMessages(withNetworkParams: Bundle): Job
|
||||
@ -75,4 +78,42 @@ interface ChatMessageRepository : LifecycleAwareManager {
|
||||
* Destroys unused resources.
|
||||
*/
|
||||
fun handleChatOnBackPress()
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun sendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>>
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun resendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>>
|
||||
|
||||
suspend fun addTemporaryMessage(
|
||||
message: CharSequence,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>>
|
||||
|
||||
suspend fun editChatMessage(credentials: String, url: String, text: String): Flow<Result<ChatOverallSingleMessage>>
|
||||
|
||||
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
|
||||
|
||||
suspend fun sendTempChatMessages(credentials: String, url: String)
|
||||
|
||||
suspend fun deleteTempMessage(chatMessage: ChatMessage)
|
||||
}
|
||||
|
@ -115,11 +115,16 @@ data class ChatMessage(
|
||||
|
||||
var openWhenDownloaded: Boolean = true,
|
||||
|
||||
var isTempMessage: Boolean = false,
|
||||
var isTemporary: Boolean = false,
|
||||
|
||||
var tempMessageId: Int = -1
|
||||
var referenceId: String? = null,
|
||||
|
||||
) : MessageContentType, MessageContentType.Image {
|
||||
var sendingFailed: Boolean = true,
|
||||
|
||||
var silent: Boolean = false
|
||||
|
||||
) : MessageContentType,
|
||||
MessageContentType.Image {
|
||||
|
||||
var extractedUrlToPreview: String? = null
|
||||
|
||||
@ -240,8 +245,8 @@ data class ChatMessage(
|
||||
}
|
||||
}
|
||||
|
||||
fun getCalculateMessageType(): MessageType {
|
||||
return if (!TextUtils.isEmpty(systemMessage)) {
|
||||
fun getCalculateMessageType(): MessageType =
|
||||
if (!TextUtils.isEmpty(systemMessage)) {
|
||||
MessageType.SYSTEM_MESSAGE
|
||||
} else if (isVoiceMessage) {
|
||||
MessageType.VOICE_MESSAGE
|
||||
@ -256,19 +261,15 @@ data class ChatMessage(
|
||||
} else {
|
||||
MessageType.REGULAR_TEXT_MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return jsonMessageId.toString()
|
||||
}
|
||||
override fun getId(): String = jsonMessageId.toString()
|
||||
|
||||
override fun getText(): String {
|
||||
return if (message != null) {
|
||||
override fun getText(): String =
|
||||
if (message != null) {
|
||||
getParsedMessage(message, messageParameters)!!
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun getNullsafeActorDisplayName() =
|
||||
if (!TextUtils.isEmpty(actorDisplayName)) {
|
||||
@ -277,22 +278,19 @@ data class ChatMessage(
|
||||
sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
|
||||
override fun getUser(): IUser {
|
||||
return object : IUser {
|
||||
override fun getId(): String {
|
||||
return "$actorType/$actorId"
|
||||
}
|
||||
override fun getUser(): IUser =
|
||||
object : IUser {
|
||||
override fun getId(): String = "$actorType/$actorId"
|
||||
|
||||
override fun getName(): String {
|
||||
return if (!TextUtils.isEmpty(actorDisplayName)) {
|
||||
override fun getName(): String =
|
||||
if (!TextUtils.isEmpty(actorDisplayName)) {
|
||||
actorDisplayName!!
|
||||
} else {
|
||||
sharedApplication!!.getString(R.string.nc_guest)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvatar(): String? {
|
||||
return when {
|
||||
override fun getAvatar(): String? =
|
||||
when {
|
||||
activeUser == null -> {
|
||||
null
|
||||
}
|
||||
@ -317,21 +315,14 @@ data class ChatMessage(
|
||||
ApiUtils.getUrlForGuestAvatar(activeUser!!.baseUrl!!, apiId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCreatedAt(): Date {
|
||||
return Date(timestamp * MILLIES)
|
||||
}
|
||||
override fun getCreatedAt(): Date = Date(timestamp * MILLIES)
|
||||
|
||||
override fun getSystemMessage(): String {
|
||||
return EnumSystemMessageTypeConverter().convertToString(systemMessageType)
|
||||
}
|
||||
override fun getSystemMessage(): String = EnumSystemMessageTypeConverter().convertToString(systemMessageType)
|
||||
|
||||
private fun isHashMapEntryEqualTo(map: HashMap<String?, String?>, key: String, searchTerm: String): Boolean {
|
||||
return map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray())
|
||||
}
|
||||
private fun isHashMapEntryEqualTo(map: HashMap<String?, String?>, key: String, searchTerm: String): Boolean =
|
||||
map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray())
|
||||
|
||||
// needed a equals and hashcode function to fix detekt errors
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@ -340,9 +331,7 @@ data class ChatMessage(
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 0
|
||||
}
|
||||
override fun hashCode(): Int = 0
|
||||
|
||||
val isVoiceMessage: Boolean
|
||||
get() = "voice-message" == messageType
|
||||
|
@ -38,7 +38,7 @@ interface ChatNetworkDataSource {
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String
|
||||
): Observable<GenericOverall> // last two fields are false
|
||||
): Observable<ChatOverallSingleMessage>
|
||||
|
||||
fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable<RoomsOverall>
|
||||
fun shareLocationToNotes(
|
||||
@ -50,19 +50,20 @@ interface ChatNetworkDataSource {
|
||||
): Observable<GenericOverall>
|
||||
|
||||
fun leaveRoom(credentials: String, url: String): Observable<GenericOverall>
|
||||
fun sendChatMessage(
|
||||
suspend fun sendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: CharSequence,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean
|
||||
): Observable<GenericOverall>
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): ChatOverallSingleMessage
|
||||
|
||||
fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap<String, Int>): Observable<Response<*>>
|
||||
fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage>
|
||||
fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall>
|
||||
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
|
||||
fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage>
|
||||
suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage
|
||||
suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall
|
||||
}
|
||||
|
@ -21,12 +21,16 @@ import com.nextcloud.talk.data.database.model.ChatBlockEntity
|
||||
import com.nextcloud.talk.data.database.model.ChatMessageEntity
|
||||
import com.nextcloud.talk.data.network.NetworkMonitor
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.extensions.toIntOrZero
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
|
||||
import com.nextcloud.talk.models.json.participants.Participant
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.nextcloud.talk.utils.message.SendMessageUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -36,18 +40,22 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
class OfflineFirstChatRepository @Inject constructor(
|
||||
private val chatDao: ChatMessagesDao,
|
||||
private val chatBlocksDao: ChatBlocksDao,
|
||||
private val network: ChatNetworkDataSource,
|
||||
private val datastore: AppPreferences,
|
||||
private val monitor: NetworkMonitor,
|
||||
private val userProvider: CurrentUserProviderNew
|
||||
userProvider: CurrentUserProviderNew
|
||||
) : ChatMessageRepository {
|
||||
|
||||
val currentUser: User = userProvider.currentUser.blockingGet()
|
||||
@ -71,8 +79,7 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
>
|
||||
> = MutableSharedFlow()
|
||||
|
||||
override val updateMessageFlow:
|
||||
Flow<ChatMessage>
|
||||
override val updateMessageFlow: Flow<ChatMessage>
|
||||
get() = _updateMessageFlow
|
||||
|
||||
private val _updateMessageFlow:
|
||||
@ -85,8 +92,7 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
private val _lastCommonReadFlow:
|
||||
MutableSharedFlow<Int> = MutableSharedFlow()
|
||||
|
||||
override val lastReadMessageFlow:
|
||||
Flow<Int>
|
||||
override val lastReadMessageFlow: Flow<Int>
|
||||
get() = _lastReadMessageFlow
|
||||
|
||||
private val _lastReadMessageFlow:
|
||||
@ -97,6 +103,12 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
|
||||
private val _generalUIFlow: MutableSharedFlow<String> = MutableSharedFlow()
|
||||
|
||||
override val removeMessageFlow: Flow<ChatMessage>
|
||||
get() = _removeMessageFlow
|
||||
|
||||
private val _removeMessageFlow:
|
||||
MutableSharedFlow<ChatMessage> = MutableSharedFlow()
|
||||
|
||||
private var newXChatLastCommonRead: Int? = null
|
||||
private var itIsPaused = false
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@ -169,26 +181,39 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb")
|
||||
}
|
||||
|
||||
if (newestMessageIdFromDb.toInt() != 0) {
|
||||
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
|
||||
|
||||
showMessagesBeforeAndEqual(
|
||||
internalConversationId,
|
||||
newestMessageIdFromDb,
|
||||
limit
|
||||
)
|
||||
|
||||
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
|
||||
// with them (otherwise there is a race condition).
|
||||
delay(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED)
|
||||
|
||||
updateUiForLastCommonRead()
|
||||
updateUiForLastReadMessage(newestMessageIdFromDb)
|
||||
}
|
||||
handleMessagesFromDb(newestMessageIdFromDb)
|
||||
|
||||
initMessagePolling(newestMessageIdFromDb)
|
||||
}
|
||||
|
||||
private suspend fun handleMessagesFromDb(newestMessageIdFromDb: Long) {
|
||||
if (newestMessageIdFromDb.toInt() != 0) {
|
||||
val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb)
|
||||
|
||||
val list = getMessagesBeforeAndEqual(
|
||||
newestMessageIdFromDb,
|
||||
internalConversationId,
|
||||
limit
|
||||
)
|
||||
if (list.isNotEmpty()) {
|
||||
handleNewAndTempMessages(
|
||||
receivedChatMessages = list,
|
||||
lookIntoFuture = false,
|
||||
showUnreadMessagesMarker = false
|
||||
)
|
||||
}
|
||||
|
||||
sendTempChatMessages(credentials, urlForChatting)
|
||||
|
||||
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
|
||||
// with them (otherwise there is a race condition).
|
||||
delay(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED)
|
||||
|
||||
updateUiForLastCommonRead()
|
||||
updateUiForLastReadMessage(newestMessageIdFromDb)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCappedMessagesAmountOfChatBlock(messageId: Long): Int {
|
||||
val chatBlock = getBlockOfMessage(messageId.toInt())
|
||||
|
||||
@ -293,8 +318,11 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
val weHaveMessagesFromOurself = chatMessages.any { it.actorId == currentUser.userId }
|
||||
showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself
|
||||
|
||||
val triple = Triple(true, showUnreadMessagesMarker, chatMessages)
|
||||
_messageFlow.emit(triple)
|
||||
handleNewAndTempMessages(
|
||||
receivedChatMessages = chatMessages,
|
||||
lookIntoFuture = true,
|
||||
showUnreadMessagesMarker = showUnreadMessagesMarker
|
||||
)
|
||||
} else {
|
||||
Log.d(TAG, "resultsFromSync are null or empty")
|
||||
}
|
||||
@ -317,6 +345,39 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleNewAndTempMessages(
|
||||
receivedChatMessages: List<ChatMessage>,
|
||||
lookIntoFuture: Boolean,
|
||||
showUnreadMessagesMarker: Boolean
|
||||
) {
|
||||
// remove all temp messages from UI
|
||||
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
|
||||
.first()
|
||||
.map(ChatMessageEntity::asModel)
|
||||
oldTempMessages.forEach { _removeMessageFlow.emit(it) }
|
||||
|
||||
// add new messages to UI
|
||||
val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages)
|
||||
_messageFlow.emit(tripleChatMessages)
|
||||
|
||||
// remove temp messages from DB that are now found in the new messages
|
||||
val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId }
|
||||
val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds }
|
||||
chatDao.deleteTempChatMessages(
|
||||
internalConversationId,
|
||||
tempChatMessagesThatCanBeReplaced.map { it.referenceId!! }
|
||||
)
|
||||
|
||||
// add the remaining temp messages to UI again
|
||||
val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
|
||||
.first()
|
||||
.sortedBy { it.internalId }
|
||||
.map(ChatMessageEntity::asModel)
|
||||
|
||||
val triple = Triple(true, false, remainingTempMessages)
|
||||
_messageFlow.emit(triple)
|
||||
}
|
||||
|
||||
private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long): Boolean {
|
||||
val loadFromServer: Boolean
|
||||
|
||||
@ -684,31 +745,18 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showMessagesBeforeAndEqual(internalConversationId: String, messageId: Long, limit: Int) {
|
||||
suspend fun getMessagesBeforeAndEqual(
|
||||
messageId: Long,
|
||||
internalConversationId: String,
|
||||
messageLimit: Int
|
||||
): List<ChatMessage> =
|
||||
chatDao.getMessagesForConversationBeforeAndEqual(
|
||||
internalConversationId,
|
||||
messageId,
|
||||
messageLimit
|
||||
).map {
|
||||
it.map(ChatMessageEntity::asModel)
|
||||
}.first()
|
||||
|
||||
val list = getMessagesBeforeAndEqual(
|
||||
messageId,
|
||||
suspend fun getMessagesBeforeAndEqual(
|
||||
messageId: Long,
|
||||
internalConversationId: String,
|
||||
messageLimit: Int
|
||||
): List<ChatMessage> =
|
||||
chatDao.getMessagesForConversationBeforeAndEqual(
|
||||
internalConversationId,
|
||||
limit
|
||||
)
|
||||
|
||||
if (list.isNotEmpty()) {
|
||||
val triple = Triple(false, false, list)
|
||||
_messageFlow.emit(triple)
|
||||
}
|
||||
}
|
||||
messageId,
|
||||
messageLimit
|
||||
).map {
|
||||
it.map(ChatMessageEntity::asModel)
|
||||
}.first()
|
||||
|
||||
private suspend fun showMessagesBefore(internalConversationId: String, messageId: Long, limit: Int) {
|
||||
suspend fun getMessagesBefore(
|
||||
@ -752,6 +800,227 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
override suspend fun sendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>> {
|
||||
if (!monitor.isOnline.first()) {
|
||||
return flow {
|
||||
emit(Result.failure(IOException("Skipped to send message as device is offline")))
|
||||
}
|
||||
}
|
||||
|
||||
return flow {
|
||||
val response = network.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
)
|
||||
|
||||
val chatMessageModel = response.ocs?.data?.asModel()
|
||||
|
||||
emit(Result.success(chatMessageModel))
|
||||
}
|
||||
.retryWhen { cause, attempt ->
|
||||
if (cause is IOException && attempt < SEND_MESSAGE_RETRY_ATTEMPTS) {
|
||||
delay(SEND_MESSAGE_RETRY_DELAY)
|
||||
return@retryWhen true
|
||||
} else {
|
||||
return@retryWhen false
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
Log.e(TAG, "Error when sending message", e)
|
||||
|
||||
val failedMessage = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
|
||||
failedMessage.sendingFailed = true
|
||||
chatDao.updateChatMessage(failedMessage)
|
||||
|
||||
val failedMessageModel = failedMessage.asModel()
|
||||
_updateMessageFlow.emit(failedMessageModel)
|
||||
|
||||
emit(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
override suspend fun resendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>> {
|
||||
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
|
||||
messageToResend.sendingFailed = false
|
||||
chatDao.updateChatMessage(messageToResend)
|
||||
|
||||
val messageToResendModel = messageToResend.asModel()
|
||||
_updateMessageFlow.emit(messageToResendModel)
|
||||
|
||||
return sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
override suspend fun editChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
text: String
|
||||
): Flow<Result<ChatOverallSingleMessage>> =
|
||||
flow {
|
||||
try {
|
||||
val response = network.editChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
text
|
||||
)
|
||||
emit(Result.success(response))
|
||||
} catch (e: Exception) {
|
||||
emit(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
override suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean> =
|
||||
flow {
|
||||
try {
|
||||
val messageToEdit = chatDao.getChatMessageForConversation(
|
||||
internalConversationId,
|
||||
message.jsonMessageId
|
||||
.toLong()
|
||||
).first()
|
||||
messageToEdit.message = editedMessageText
|
||||
chatDao.upsertChatMessage(messageToEdit)
|
||||
|
||||
val editedMessageModel = messageToEdit.asModel()
|
||||
_updateMessageFlow.emit(editedMessageModel)
|
||||
emit(true)
|
||||
} catch (e: Exception) {
|
||||
emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendTempChatMessages(credentials: String, url: String) {
|
||||
val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first()
|
||||
tempMessages.sortedBy { it.internalId }.onEach {
|
||||
sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
it.message,
|
||||
it.actorDisplayName,
|
||||
it.parentMessageId?.toIntOrZero() ?: 0,
|
||||
it.silent,
|
||||
it.referenceId.orEmpty()
|
||||
).collect { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "Sent temp message")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send temp message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteTempMessage(chatMessage: ChatMessage) {
|
||||
chatDao.deleteTempChatMessages(internalConversationId, listOf(chatMessage.referenceId.orEmpty()))
|
||||
_removeMessageFlow.emit(chatMessage)
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
override suspend fun addTemporaryMessage(
|
||||
message: CharSequence,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): Flow<Result<ChatMessage?>> =
|
||||
flow {
|
||||
try {
|
||||
val tempChatMessageEntity = createChatMessageEntity(
|
||||
internalConversationId,
|
||||
message.toString(),
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
)
|
||||
|
||||
chatDao.upsertChatMessage(tempChatMessageEntity)
|
||||
|
||||
val tempChatMessageModel = tempChatMessageEntity.asModel()
|
||||
|
||||
emit(Result.success(tempChatMessageModel))
|
||||
|
||||
val triple = Triple(true, false, listOf(tempChatMessageModel))
|
||||
_messageFlow.emit(triple)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Something went wrong when adding temporary message", e)
|
||||
emit(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessageEntity(
|
||||
internalConversationId: String,
|
||||
message: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): ChatMessageEntity {
|
||||
val currentTimeMillies = System.currentTimeMillis()
|
||||
|
||||
val currentTimeWithoutYear = SendMessageUtils().removeYearFromTimestamp(currentTimeMillies)
|
||||
|
||||
val parentMessageId = if (replyTo != 0) {
|
||||
replyTo.toLong()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val entity = ChatMessageEntity(
|
||||
internalId = "$internalConversationId@_temp_$currentTimeMillies",
|
||||
internalConversationId = internalConversationId,
|
||||
id = currentTimeWithoutYear.toLong(),
|
||||
message = message,
|
||||
deleted = false,
|
||||
token = conversationModel.token,
|
||||
actorId = currentUser.userId!!,
|
||||
actorType = EnumActorTypeConverter().convertToString(Participant.ActorType.USERS),
|
||||
accountId = currentUser.id!!,
|
||||
messageParameters = null,
|
||||
messageType = "comment",
|
||||
parentMessageId = parentMessageId,
|
||||
systemMessageType = ChatMessage.SystemMessageType.DUMMY,
|
||||
replyable = false,
|
||||
timestamp = currentTimeMillies / MILLIES,
|
||||
expirationTimestamp = 0,
|
||||
actorDisplayName = currentUser.displayName!!,
|
||||
referenceId = referenceId,
|
||||
isTemporary = true,
|
||||
sendingFailed = false,
|
||||
silent = sendWithoutNotification
|
||||
)
|
||||
return entity
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = OfflineFirstChatRepository::class.simpleName
|
||||
private const val HTTP_CODE_OK: Int = 200
|
||||
@ -760,5 +1029,8 @@ class OfflineFirstChatRepository @Inject constructor(
|
||||
private const val HALF_SECOND = 500L
|
||||
private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100
|
||||
private const val DEFAULT_MESSAGES_LIMIT = 100
|
||||
private const val MILLIES = 1000
|
||||
private const val SEND_MESSAGE_RETRY_ATTEMPTS = 3
|
||||
private const val SEND_MESSAGE_RETRY_DELAY: Long = 2000
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
import com.nextcloud.talk.models.json.reminder.Reminder
|
||||
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.message.SendMessageUtils
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.Response
|
||||
|
||||
@ -108,26 +109,24 @@ class RetrofitChatNetwork(
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String
|
||||
): Observable<GenericOverall> {
|
||||
return ncApi.sendChatMessage(
|
||||
): Observable<ChatOverallSingleMessage> =
|
||||
ncApi.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
null,
|
||||
false
|
||||
false,
|
||||
SendMessageUtils().generateReferenceId()
|
||||
).map {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkForNoteToSelf(
|
||||
credentials: String,
|
||||
url: String,
|
||||
includeStatus: Boolean
|
||||
): Observable<RoomsOverall> {
|
||||
return ncApi.getRooms(credentials, url, includeStatus).map { it }
|
||||
}
|
||||
): Observable<RoomsOverall> = ncApi.getRooms(credentials, url, includeStatus).map { it }
|
||||
|
||||
override fun shareLocationToNotes(
|
||||
credentials: String,
|
||||
@ -135,54 +134,56 @@ class RetrofitChatNetwork(
|
||||
objectType: String,
|
||||
objectId: String,
|
||||
metadata: String
|
||||
): Observable<GenericOverall> {
|
||||
return ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it }
|
||||
}
|
||||
): Observable<GenericOverall> = ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it }
|
||||
|
||||
override fun leaveRoom(credentials: String, url: String): Observable<GenericOverall> {
|
||||
return ncApi.leaveRoom(credentials, url).map { it }
|
||||
}
|
||||
|
||||
override fun sendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: CharSequence,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean
|
||||
): Observable<GenericOverall> {
|
||||
return ncApi.sendChatMessage(credentials, url, message, displayName, replyTo, sendWithoutNotification).map {
|
||||
override fun leaveRoom(credentials: String, url: String): Observable<GenericOverall> =
|
||||
ncApi.leaveRoom(credentials, url).map {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendChatMessage(
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean,
|
||||
referenceId: String
|
||||
): ChatOverallSingleMessage =
|
||||
ncApiCoroutines.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
)
|
||||
|
||||
override fun pullChatMessages(
|
||||
credentials: String,
|
||||
url: String,
|
||||
fieldMap: HashMap<String, Int>
|
||||
): Observable<Response<*>> {
|
||||
return ncApi.pullChatMessages(credentials, url, fieldMap).map { it }
|
||||
}
|
||||
): Observable<Response<*>> = ncApi.pullChatMessages(credentials, url, fieldMap).map { it }
|
||||
|
||||
override fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage> {
|
||||
return ncApi.deleteChatMessage(credentials, url).map { it }
|
||||
}
|
||||
override fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage> =
|
||||
ncApi.deleteChatMessage(credentials, url).map {
|
||||
it
|
||||
}
|
||||
|
||||
override fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall> {
|
||||
return ncApi.createRoom(credentials, url, map).map { it }
|
||||
}
|
||||
override fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall> =
|
||||
ncApi.createRoom(credentials, url, map).map {
|
||||
it
|
||||
}
|
||||
|
||||
override fun setChatReadMarker(
|
||||
credentials: String,
|
||||
url: String,
|
||||
previousMessageId: Int
|
||||
): Observable<GenericOverall> {
|
||||
return ncApi.setChatReadMarker(credentials, url, previousMessageId).map { it }
|
||||
}
|
||||
): Observable<GenericOverall> = ncApi.setChatReadMarker(credentials, url, previousMessageId).map { it }
|
||||
|
||||
override fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage> {
|
||||
return ncApi.editChatMessage(credentials, url, text).map { it }
|
||||
}
|
||||
override suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage =
|
||||
ncApiCoroutines.editChatMessage(credentials, url, text)
|
||||
|
||||
override suspend fun getOutOfOfficeStatusForUser(
|
||||
credentials: String,
|
||||
|
@ -24,6 +24,7 @@ import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
|
||||
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.extensions.toIntOrZero
|
||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||
import com.nextcloud.talk.models.domain.ConversationModel
|
||||
import com.nextcloud.talk.models.domain.ReactionAddedModel
|
||||
@ -63,7 +64,8 @@ class ChatViewModel @Inject constructor(
|
||||
private val mediaRecorderManager: MediaRecorderManager,
|
||||
private val audioFocusRequestManager: AudioFocusRequestManager,
|
||||
private val userProvider: CurrentUserProviderNew
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
) : ViewModel(),
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
enum class LifeCycleFlag {
|
||||
PAUSED,
|
||||
@ -74,6 +76,10 @@ class ChatViewModel @Inject constructor(
|
||||
lateinit var currentLifeCycleFlag: LifeCycleFlag
|
||||
val disposableSet = mutableSetOf<Disposable>()
|
||||
|
||||
fun getChatRepository(): ChatMessageRepository {
|
||||
return chatRepository
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
currentLifeCycleFlag = LifeCycleFlag.RESUMED
|
||||
@ -131,6 +137,8 @@ class ChatViewModel @Inject constructor(
|
||||
_chatMessageViewState.value = ChatMessageErrorState
|
||||
}
|
||||
|
||||
val getRemoveMessageFlow = chatRepository.removeMessageFlow
|
||||
|
||||
val getUpdateMessageFlow = chatRepository.updateMessageFlow
|
||||
|
||||
val getLastCommonReadFlow = chatRepository.lastCommonReadFlow
|
||||
@ -239,14 +247,9 @@ class ChatViewModel @Inject constructor(
|
||||
chatRepository.setData(conversationModel, credentials, urlForChatting)
|
||||
}
|
||||
|
||||
fun getRoom(user: User, token: String) {
|
||||
fun getRoom(token: String) {
|
||||
_getRoomViewState.value = GetRoomStartState
|
||||
conversationRepository.getRoom(token)
|
||||
|
||||
// chatNetworkDataSource.getRoom(user, token)
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// ?.observeOn(AndroidSchedulers.mainThread())
|
||||
// ?.subscribe(GetRoomObserver())
|
||||
}
|
||||
|
||||
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
|
||||
@ -478,12 +481,12 @@ class ChatViewModel @Inject constructor(
|
||||
chatNetworkDataSource.shareToNotes(credentials, url, message, displayName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(object : Observer<GenericOverall> {
|
||||
?.subscribe(object : Observer<ChatOverallSingleMessage> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposableSet.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(genericOverall: GenericOverall) {
|
||||
override fun onNext(genericOverall: ChatOverallSingleMessage) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
@ -609,9 +612,7 @@ class ChatViewModel @Inject constructor(
|
||||
cachedFile.delete()
|
||||
}
|
||||
|
||||
fun getCurrentVoiceRecordFile(): String {
|
||||
return mediaRecorderManager.currentVoiceRecordFile
|
||||
}
|
||||
fun getCurrentVoiceRecordFile(): String = mediaRecorderManager.currentVoiceRecordFile
|
||||
|
||||
fun uploadFile(fileUri: String, room: String, displayName: String, metaData: String) {
|
||||
try {
|
||||
@ -650,7 +651,7 @@ class ChatViewModel @Inject constructor(
|
||||
chatRepository.handleChatOnBackPress()
|
||||
}
|
||||
|
||||
suspend fun getMessageById(url: String, conversationModel: ConversationModel, messageId: Long): Flow<ChatMessage> =
|
||||
fun getMessageById(url: String, conversationModel: ConversationModel, messageId: Long): Flow<ChatMessage> =
|
||||
flow {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(BundleKeys.KEY_CHAT_URL, url)
|
||||
@ -671,25 +672,6 @@ class ChatViewModel @Inject constructor(
|
||||
fun getPlaybackSpeedPreference(message: ChatMessage) =
|
||||
_voiceMessagePlaybackSpeedPreferences.value?.get(message.user.id) ?: PlaybackSpeed.NORMAL
|
||||
|
||||
// inner class GetRoomObserver : Observer<ConversationModel> {
|
||||
// override fun onSubscribe(d: Disposable) {
|
||||
// // unused atm
|
||||
// }
|
||||
//
|
||||
// override fun onNext(conversationModel: ConversationModel) {
|
||||
// _getRoomViewState.value = GetRoomSuccessState(conversationModel)
|
||||
// }
|
||||
//
|
||||
// override fun onError(e: Throwable) {
|
||||
// Log.e(TAG, "Error when fetching room")
|
||||
// _getRoomViewState.value = GetRoomErrorState
|
||||
// }
|
||||
//
|
||||
// override fun onComplete() {
|
||||
// // unused atm
|
||||
// }
|
||||
// }
|
||||
|
||||
inner class JoinRoomObserver : Observer<ConversationModel> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposableSet.add(d)
|
||||
@ -788,6 +770,32 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTempMessage(chatMessage: ChatMessage) {
|
||||
viewModelScope.launch {
|
||||
chatRepository.deleteTempMessage(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) {
|
||||
viewModelScope.launch {
|
||||
chatRepository.resendChatMessage(
|
||||
credentials,
|
||||
urlForChat,
|
||||
message.message.orEmpty(),
|
||||
message.actorDisplayName.orEmpty(),
|
||||
message.parentMessageId?.toIntOrZero() ?: 0,
|
||||
false,
|
||||
message.referenceId.orEmpty()
|
||||
).collect { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "resend successful")
|
||||
} else {
|
||||
Log.e(TAG, "resend failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ChatViewModel::class.simpleName
|
||||
const val JOIN_ROOM_RETRY_COUNT: Long = 3
|
||||
|
@ -14,50 +14,40 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.nextcloud.talk.chat.data.ChatMessageRepository
|
||||
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
|
||||
import com.nextcloud.talk.chat.data.io.AudioRecorderManager
|
||||
import com.nextcloud.talk.chat.data.io.MediaPlayerManager
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import com.nextcloud.talk.utils.message.SendMessageUtils
|
||||
import com.stfalcon.chatkit.commons.models.IMessage
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.lang.Thread.sleep
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("Detekt.TooManyFunctions")
|
||||
class MessageInputViewModel @Inject constructor(
|
||||
private val chatNetworkDataSource: ChatNetworkDataSource,
|
||||
private val audioRecorderManager: AudioRecorderManager,
|
||||
private val mediaPlayerManager: MediaPlayerManager,
|
||||
private val audioFocusRequestManager: AudioFocusRequestManager,
|
||||
private val appPreferences: AppPreferences
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
private val audioFocusRequestManager: AudioFocusRequestManager
|
||||
) : ViewModel(),
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
enum class LifeCycleFlag {
|
||||
PAUSED,
|
||||
RESUMED,
|
||||
STOPPED
|
||||
}
|
||||
|
||||
lateinit var chatRepository: ChatMessageRepository
|
||||
lateinit var currentLifeCycleFlag: LifeCycleFlag
|
||||
val disposableSet = mutableSetOf<Disposable>()
|
||||
|
||||
data class QueuedMessage(
|
||||
val id: Int,
|
||||
var message: CharSequence? = null,
|
||||
val displayName: String? = null,
|
||||
val replyTo: Int? = null,
|
||||
val sendWithoutNotification: Boolean? = null
|
||||
)
|
||||
|
||||
private var isQueueing: Boolean = false
|
||||
private var messageQueue: MutableList<QueuedMessage> = mutableListOf()
|
||||
fun setData(chatMessageRepository: ChatMessageRepository) {
|
||||
chatRepository = chatMessageRepository
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
@ -106,11 +96,12 @@ class MessageInputViewModel @Inject constructor(
|
||||
sealed interface ViewState
|
||||
object SendChatMessageStartState : ViewState
|
||||
class SendChatMessageSuccessState(val message: CharSequence) : ViewState
|
||||
class SendChatMessageErrorState(val e: Throwable, val message: CharSequence) : ViewState
|
||||
class SendChatMessageErrorState(val message: CharSequence) : ViewState
|
||||
|
||||
private val _sendChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(SendChatMessageStartState)
|
||||
val sendChatMessageViewState: LiveData<ViewState>
|
||||
get() = _sendChatMessageViewState
|
||||
object EditMessageStartState : ViewState
|
||||
|
||||
object EditMessageErrorState : ViewState
|
||||
class EditMessageSuccessState(val messageEdited: ChatOverallSingleMessage) : ViewState
|
||||
|
||||
@ -122,89 +113,93 @@ class MessageInputViewModel @Inject constructor(
|
||||
val isVoicePreviewPlaying: LiveData<Boolean>
|
||||
get() = _isVoicePreviewPlaying
|
||||
|
||||
private val _messageQueueSizeFlow = MutableStateFlow(messageQueue.size)
|
||||
val messageQueueSizeFlow: LiveData<Int>
|
||||
get() = _messageQueueSizeFlow.asLiveData()
|
||||
|
||||
private val _messageQueueFlow: MutableLiveData<List<QueuedMessage>> = MutableLiveData()
|
||||
val messageQueueFlow: LiveData<List<QueuedMessage>>
|
||||
get() = _messageQueueFlow
|
||||
|
||||
private val _callStartedFlow: MutableLiveData<Pair<ChatMessage, Boolean>> = MutableLiveData()
|
||||
val callStartedFlow: LiveData<Pair<ChatMessage, Boolean>>
|
||||
get() = _callStartedFlow
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun sendChatMessage(
|
||||
internalId: String,
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: CharSequence,
|
||||
message: String,
|
||||
displayName: String,
|
||||
replyTo: Int,
|
||||
sendWithoutNotification: Boolean
|
||||
) {
|
||||
if (isQueueing) {
|
||||
val tempID = System.currentTimeMillis().toInt()
|
||||
val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification)
|
||||
messageQueue = appPreferences.getMessageQueue(internalId)
|
||||
messageQueue.add(qMsg)
|
||||
appPreferences.saveMessageQueue(internalId, messageQueue)
|
||||
_messageQueueSizeFlow.update { messageQueue.size }
|
||||
_messageQueueFlow.postValue(listOf(qMsg))
|
||||
return
|
||||
val referenceId = SendMessageUtils().generateReferenceId()
|
||||
Log.d(TAG, "Random SHA-256 Hash: $referenceId")
|
||||
|
||||
viewModelScope.launch {
|
||||
chatRepository.addTemporaryMessage(
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
).collect { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "temp message ref id: " + (result.getOrNull()?.referenceId ?: "none"))
|
||||
|
||||
_sendChatMessageViewState.value = SendChatMessageSuccessState(message)
|
||||
} else {
|
||||
_sendChatMessageViewState.value = SendChatMessageErrorState(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatNetworkDataSource.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification
|
||||
).subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(object : Observer<GenericOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposableSet.add(d)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
chatRepository.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
message,
|
||||
displayName,
|
||||
replyTo,
|
||||
sendWithoutNotification,
|
||||
referenceId
|
||||
).collect { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "received ref id: " + (result.getOrNull()?.referenceId ?: "none"))
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
_sendChatMessageViewState.value = SendChatMessageErrorState(e, message)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(t: GenericOverall) {
|
||||
_sendChatMessageViewState.value = SendChatMessageSuccessState(message)
|
||||
} else {
|
||||
_sendChatMessageViewState.value = SendChatMessageErrorState(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTempMessages(credentials: String, url: String) {
|
||||
viewModelScope.launch {
|
||||
chatRepository.sendTempChatMessages(
|
||||
credentials,
|
||||
url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun editChatMessage(credentials: String, url: String, text: String) {
|
||||
chatNetworkDataSource.editChatMessage(credentials, url, text)
|
||||
.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(object : Observer<ChatOverallSingleMessage> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
disposableSet.add(d)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "failed to edit message", e)
|
||||
viewModelScope.launch {
|
||||
chatRepository.editChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
text
|
||||
).collect { result ->
|
||||
if (result.isSuccess) {
|
||||
_editMessageViewState.value = EditMessageSuccessState(result.getOrNull()!!)
|
||||
} else {
|
||||
_editMessageViewState.value = EditMessageErrorState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(messageEdited: ChatOverallSingleMessage) {
|
||||
_editMessageViewState.value = EditMessageSuccessState(messageEdited)
|
||||
}
|
||||
})
|
||||
fun editTempChatMessage(message: ChatMessage, editedMessageText: String) {
|
||||
viewModelScope.launch {
|
||||
chatRepository.editTempChatMessage(
|
||||
message,
|
||||
editedMessageText
|
||||
).collect {}
|
||||
}
|
||||
}
|
||||
|
||||
fun reply(message: IMessage?) {
|
||||
@ -256,75 +251,11 @@ class MessageInputViewModel @Inject constructor(
|
||||
_getRecordingTime.postValue(time)
|
||||
}
|
||||
|
||||
fun sendAndEmptyMessageQueue(internalId: String, credentials: String, url: String) {
|
||||
if (isQueueing) return
|
||||
messageQueue.clear()
|
||||
|
||||
val queue = appPreferences.getMessageQueue(internalId)
|
||||
appPreferences.saveMessageQueue(internalId, null) // empties the queue
|
||||
while (queue.size > 0) {
|
||||
val msg = queue.removeAt(0)
|
||||
sendChatMessage(
|
||||
internalId,
|
||||
credentials,
|
||||
url,
|
||||
msg.message!!,
|
||||
msg.displayName!!,
|
||||
msg.replyTo!!,
|
||||
msg.sendWithoutNotification!!
|
||||
)
|
||||
sleep(DELAY_BETWEEN_QUEUED_MESSAGES)
|
||||
}
|
||||
_messageQueueSizeFlow.tryEmit(0)
|
||||
}
|
||||
|
||||
fun getTempMessagesFromMessageQueue(internalId: String) {
|
||||
val queue = appPreferences.getMessageQueue(internalId)
|
||||
val list = mutableListOf<QueuedMessage>()
|
||||
for (msg in queue) {
|
||||
list.add(msg)
|
||||
}
|
||||
_messageQueueFlow.postValue(list)
|
||||
}
|
||||
|
||||
fun switchToMessageQueue(shouldQueue: Boolean) {
|
||||
isQueueing = shouldQueue
|
||||
}
|
||||
|
||||
fun restoreMessageQueue(internalId: String) {
|
||||
messageQueue = appPreferences.getMessageQueue(internalId)
|
||||
_messageQueueSizeFlow.tryEmit(messageQueue.size)
|
||||
}
|
||||
|
||||
fun removeFromQueue(internalId: String, id: Int) {
|
||||
val queue = appPreferences.getMessageQueue(internalId)
|
||||
for (qMsg in queue) {
|
||||
if (qMsg.id == id) {
|
||||
queue.remove(qMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
appPreferences.saveMessageQueue(internalId, queue)
|
||||
_messageQueueSizeFlow.tryEmit(queue.size)
|
||||
}
|
||||
|
||||
fun editQueuedMessage(internalId: String, id: Int, newMessage: String) {
|
||||
val queue = appPreferences.getMessageQueue(internalId)
|
||||
for (qMsg in queue) {
|
||||
if (qMsg.id == id) {
|
||||
qMsg.message = newMessage
|
||||
break
|
||||
}
|
||||
}
|
||||
appPreferences.saveMessageQueue(internalId, queue)
|
||||
}
|
||||
|
||||
fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) {
|
||||
_callStartedFlow.postValue(Pair(recent, show))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MessageInputViewModel::class.java.simpleName
|
||||
private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 1000
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ package com.nextcloud.talk.conversationlist.data.network
|
||||
|
||||
import android.util.Log
|
||||
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
|
||||
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
|
||||
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
|
||||
import com.nextcloud.talk.data.database.dao.ConversationsDao
|
||||
import com.nextcloud.talk.data.database.mappers.asEntity
|
||||
@ -107,7 +106,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
|
||||
var conversationsFromSync: List<ConversationEntity>? = null
|
||||
|
||||
if (!monitor.isOnline.first()) {
|
||||
Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server")
|
||||
Log.d(TAG, "Device is offline, can't load conversations from server")
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,6 @@ import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import okhttp3.OkHttpClient
|
||||
@ -74,87 +73,66 @@ class RepositoryModule {
|
||||
ncApi: NcApi,
|
||||
ncApiCoroutines: NcApiCoroutines,
|
||||
userProvider: CurrentUserProviderNew
|
||||
): ConversationsRepository {
|
||||
return ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
|
||||
}
|
||||
): ConversationsRepository = ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideSharedItemsRepository(ncApi: NcApi, dateUtils: DateUtils): SharedItemsRepository {
|
||||
return SharedItemsRepositoryImpl(ncApi, dateUtils)
|
||||
}
|
||||
fun provideSharedItemsRepository(ncApi: NcApi, dateUtils: DateUtils): SharedItemsRepository =
|
||||
SharedItemsRepositoryImpl(ncApi, dateUtils)
|
||||
|
||||
@Provides
|
||||
fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): UnifiedSearchRepository {
|
||||
return UnifiedSearchRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): UnifiedSearchRepository =
|
||||
UnifiedSearchRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository {
|
||||
return PollRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository =
|
||||
PollRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideRemoteFileBrowserItemsRepository(
|
||||
okHttpClient: OkHttpClient,
|
||||
userProvider: CurrentUserProviderNew
|
||||
): RemoteFileBrowserItemsRepository {
|
||||
return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider)
|
||||
}
|
||||
): RemoteFileBrowserItemsRepository = RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideUsersRepository(database: TalkDatabase): UsersRepository {
|
||||
return UsersRepositoryImpl(database.usersDao())
|
||||
}
|
||||
fun provideUsersRepository(database: TalkDatabase): UsersRepository = UsersRepositoryImpl(database.usersDao())
|
||||
|
||||
@Provides
|
||||
fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository {
|
||||
return ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao())
|
||||
}
|
||||
fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository =
|
||||
ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao())
|
||||
|
||||
@Provides
|
||||
fun provideReactionsRepository(
|
||||
ncApi: NcApi,
|
||||
userProvider: CurrentUserProviderNew,
|
||||
dao: ChatMessagesDao
|
||||
): ReactionsRepository {
|
||||
return ReactionsRepositoryImpl(ncApi, userProvider, dao)
|
||||
}
|
||||
): ReactionsRepository = ReactionsRepositoryImpl(ncApi, userProvider, dao)
|
||||
|
||||
@Provides
|
||||
fun provideCallRecordingRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): CallRecordingRepository {
|
||||
return CallRecordingRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
fun provideCallRecordingRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): CallRecordingRepository =
|
||||
CallRecordingRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideRequestAssistanceRepository(
|
||||
ncApi: NcApi,
|
||||
userProvider: CurrentUserProviderNew
|
||||
): RequestAssistanceRepository {
|
||||
return RequestAssistanceRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
): RequestAssistanceRepository = RequestAssistanceRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideOpenConversationsRepository(
|
||||
ncApi: NcApi,
|
||||
userProvider: CurrentUserProviderNew
|
||||
): OpenConversationsRepository {
|
||||
return OpenConversationsRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
): OpenConversationsRepository = OpenConversationsRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun translateRepository(ncApi: NcApi): TranslateRepository {
|
||||
return TranslateRepositoryImpl(ncApi)
|
||||
}
|
||||
fun translateRepository(ncApi: NcApi): TranslateRepository = TranslateRepositoryImpl(ncApi)
|
||||
|
||||
@Provides
|
||||
fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource {
|
||||
return RetrofitChatNetwork(ncApi, ncApiCoroutines)
|
||||
}
|
||||
fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource =
|
||||
RetrofitChatNetwork(ncApi, ncApiCoroutines)
|
||||
|
||||
@Provides
|
||||
fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource {
|
||||
return RetrofitConversationsNetwork(ncApi)
|
||||
}
|
||||
fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource =
|
||||
RetrofitConversationsNetwork(ncApi)
|
||||
|
||||
@Provides
|
||||
fun provideConversationInfoEditRepository(
|
||||
@ -166,33 +144,27 @@ class RepositoryModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository {
|
||||
return ConversationRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository =
|
||||
ConversationRepositoryImpl(ncApi, userProvider)
|
||||
|
||||
@Provides
|
||||
fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository {
|
||||
return InvitationsRepositoryImpl(ncApi)
|
||||
}
|
||||
fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi)
|
||||
|
||||
@Provides
|
||||
fun provideOfflineFirstChatRepository(
|
||||
chatMessagesDao: ChatMessagesDao,
|
||||
chatBlocksDao: ChatBlocksDao,
|
||||
dataSource: ChatNetworkDataSource,
|
||||
appPreferences: AppPreferences,
|
||||
networkMonitor: NetworkMonitor,
|
||||
userProvider: CurrentUserProviderNew
|
||||
): ChatMessageRepository {
|
||||
return OfflineFirstChatRepository(
|
||||
): ChatMessageRepository =
|
||||
OfflineFirstChatRepository(
|
||||
chatMessagesDao,
|
||||
chatBlocksDao,
|
||||
dataSource,
|
||||
appPreferences,
|
||||
networkMonitor,
|
||||
userProvider
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideOfflineFirstConversationsRepository(
|
||||
@ -201,26 +173,22 @@ class RepositoryModule {
|
||||
chatNetworkDataSource: ChatNetworkDataSource,
|
||||
networkMonitor: NetworkMonitor,
|
||||
currentUserProviderNew: CurrentUserProviderNew
|
||||
): OfflineConversationsRepository {
|
||||
return OfflineFirstConversationsRepository(
|
||||
): OfflineConversationsRepository =
|
||||
OfflineFirstConversationsRepository(
|
||||
dao,
|
||||
dataSource,
|
||||
chatNetworkDataSource,
|
||||
networkMonitor,
|
||||
currentUserProviderNew
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository {
|
||||
return ContactsRepositoryImpl(ncApiCoroutines, userManager)
|
||||
}
|
||||
fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository =
|
||||
ContactsRepositoryImpl(ncApiCoroutines, userManager)
|
||||
|
||||
@Provides
|
||||
fun provideConversationCreationRepository(
|
||||
ncApiCoroutines: NcApiCoroutines,
|
||||
userManager: UserManager
|
||||
): ConversationCreationRepository {
|
||||
return ConversationCreationRepositoryImpl(ncApiCoroutines, userManager)
|
||||
}
|
||||
): ConversationCreationRepository = ConversationCreationRepositoryImpl(ncApiCoroutines, userManager)
|
||||
}
|
||||
|
@ -16,12 +16,14 @@ import com.nextcloud.talk.data.database.model.ChatMessageEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@Suppress("Detekt.TooManyFunctions")
|
||||
interface ChatMessagesDao {
|
||||
@Query(
|
||||
"""
|
||||
SELECT MAX(id) as max_items
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 0
|
||||
"""
|
||||
)
|
||||
fun getNewestMessageId(internalConversationId: String): Long
|
||||
@ -31,11 +33,35 @@ interface ChatMessagesDao {
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 0
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
"""
|
||||
)
|
||||
fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 1
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
"""
|
||||
)
|
||||
fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND referenceId = :referenceId
|
||||
AND isTemporary = 1
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
"""
|
||||
)
|
||||
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)
|
||||
|
||||
@ -54,10 +80,20 @@ interface ChatMessagesDao {
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM ChatMessages
|
||||
WHERE id in (:messageIds)
|
||||
WHERE internalId in (:internalIds)
|
||||
"""
|
||||
)
|
||||
fun deleteChatMessages(messageIds: List<Int>)
|
||||
fun deleteChatMessages(internalIds: List<String>)
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND referenceId in (:referenceIds)
|
||||
AND isTemporary = 1
|
||||
"""
|
||||
)
|
||||
fun deleteTempChatMessages(internalConversationId: String, referenceIds: List<String>)
|
||||
|
||||
@Update
|
||||
fun updateChatMessage(message: ChatMessageEntity)
|
||||
@ -77,6 +113,7 @@ interface ChatMessagesDao {
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId AND id >= :messageId
|
||||
AND isTemporary = 0
|
||||
ORDER BY timestamp ASC, id ASC
|
||||
"""
|
||||
)
|
||||
@ -87,6 +124,7 @@ interface ChatMessagesDao {
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 0
|
||||
AND id < :messageId
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit
|
||||
@ -103,6 +141,7 @@ interface ChatMessagesDao {
|
||||
SELECT *
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 0
|
||||
AND id <= :messageId
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit
|
||||
@ -119,6 +158,7 @@ interface ChatMessagesDao {
|
||||
SELECT COUNT(*)
|
||||
FROM ChatMessages
|
||||
WHERE internalConversationId = :internalConversationId
|
||||
AND isTemporary = 0
|
||||
AND id BETWEEN :newestMessageId AND :oldestMessageId
|
||||
"""
|
||||
)
|
||||
|
@ -10,6 +10,7 @@ package com.nextcloud.talk.data.database.mappers
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessageJson
|
||||
import com.nextcloud.talk.data.database.model.ChatMessageEntity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||
|
||||
fun ChatMessageJson.asEntity(accountId: Long) =
|
||||
ChatMessageEntity(
|
||||
@ -37,7 +38,9 @@ fun ChatMessageJson.asEntity(accountId: Long) =
|
||||
lastEditActorId = lastEditActorId,
|
||||
lastEditActorType = lastEditActorType,
|
||||
lastEditTimestamp = lastEditTimestamp,
|
||||
deleted = deleted
|
||||
deleted = deleted,
|
||||
referenceId = referenceId,
|
||||
silent = silent
|
||||
)
|
||||
|
||||
fun ChatMessageEntity.asModel() =
|
||||
@ -62,7 +65,12 @@ fun ChatMessageEntity.asModel() =
|
||||
lastEditActorId = lastEditActorId,
|
||||
lastEditActorType = lastEditActorType,
|
||||
lastEditTimestamp = lastEditTimestamp,
|
||||
isDeleted = deleted
|
||||
isDeleted = deleted,
|
||||
referenceId = referenceId,
|
||||
isTemporary = isTemporary,
|
||||
sendingFailed = sendingFailed,
|
||||
readStatus = ReadStatus.NONE,
|
||||
silent = silent
|
||||
)
|
||||
|
||||
fun ChatMessageJson.asModel() =
|
||||
@ -87,5 +95,7 @@ fun ChatMessageJson.asModel() =
|
||||
lastEditActorId = lastEditActorId,
|
||||
lastEditActorType = lastEditActorType,
|
||||
lastEditTimestamp = lastEditTimestamp,
|
||||
isDeleted = deleted
|
||||
isDeleted = deleted,
|
||||
referenceId = referenceId,
|
||||
silent = silent
|
||||
)
|
||||
|
@ -52,6 +52,7 @@ data class ChatMessageEntity(
|
||||
@ColumnInfo(name = "deleted") var deleted: Boolean = false,
|
||||
@ColumnInfo(name = "expirationTimestamp") var expirationTimestamp: Int = 0,
|
||||
@ColumnInfo(name = "isReplyable") var replyable: Boolean = false,
|
||||
@ColumnInfo(name = "isTemporary") var isTemporary: Boolean = false,
|
||||
@ColumnInfo(name = "lastEditActorDisplayName") var lastEditActorDisplayName: String? = null,
|
||||
@ColumnInfo(name = "lastEditActorId") var lastEditActorId: String? = null,
|
||||
@ColumnInfo(name = "lastEditActorType") var lastEditActorType: String? = null,
|
||||
@ -62,8 +63,9 @@ data class ChatMessageEntity(
|
||||
@ColumnInfo(name = "parent") var parentMessageId: Long? = null,
|
||||
@ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null,
|
||||
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
|
||||
@ColumnInfo(name = "referenceId") var referenceId: String? = null,
|
||||
@ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false,
|
||||
@ColumnInfo(name = "silent") var silent: Boolean = false,
|
||||
@ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType,
|
||||
@ColumnInfo(name = "timestamp") var timestamp: Long = 0
|
||||
// missing/not needed: referenceId
|
||||
// missing/not needed: silent
|
||||
)
|
||||
|
@ -48,6 +48,13 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_12_13 = object : Migration(12, 13) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
Log.i("Migrations", "Migrating 12 to 13")
|
||||
addTempMessagesSupport(db)
|
||||
}
|
||||
}
|
||||
|
||||
fun migrateToRoom(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"CREATE TABLE User_new (" +
|
||||
@ -257,4 +264,42 @@ object Migrations {
|
||||
Log.i("Migrations", "hasArchived already exists")
|
||||
}
|
||||
}
|
||||
|
||||
fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
|
||||
try {
|
||||
db.execSQL(
|
||||
"ALTER TABLE ChatMessages " +
|
||||
"ADD COLUMN referenceId TEXT;"
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
Log.i("Migrations", "Something went wrong when adding column referenceId to table ChatMessages")
|
||||
}
|
||||
|
||||
try {
|
||||
db.execSQL(
|
||||
"ALTER TABLE ChatMessages " +
|
||||
"ADD COLUMN isTemporary INTEGER NOT NULL DEFAULT 0;"
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
Log.i("Migrations", "Something went wrong when adding column isTemporary to table ChatMessages")
|
||||
}
|
||||
|
||||
try {
|
||||
db.execSQL(
|
||||
"ALTER TABLE ChatMessages " +
|
||||
"ADD COLUMN sendingFailed INTEGER NOT NULL DEFAULT 0;"
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
Log.i("Migrations", "Something went wrong when adding column sendingFailed to table ChatMessages")
|
||||
}
|
||||
|
||||
try {
|
||||
db.execSQL(
|
||||
"ALTER TABLE ChatMessages " +
|
||||
"ADD COLUMN silent INTEGER NOT NULL DEFAULT 0;"
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
Log.i("Migrations", "Something went wrong when adding column silent to table ChatMessages")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ import java.util.Locale
|
||||
ChatMessageEntity::class,
|
||||
ChatBlockEntity::class
|
||||
],
|
||||
version = 12,
|
||||
version = 13,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 9, to = 11)
|
||||
],
|
||||
@ -114,7 +114,8 @@ abstract class TalkDatabase : RoomDatabase() {
|
||||
Migrations.MIGRATION_7_8,
|
||||
Migrations.MIGRATION_8_9,
|
||||
Migrations.MIGRATION_10_11,
|
||||
Migrations.MIGRATION_11_12
|
||||
Migrations.MIGRATION_11_12,
|
||||
Migrations.MIGRATION_12_13
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.addCallback(
|
||||
@ -128,8 +129,8 @@ abstract class TalkDatabase : RoomDatabase() {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getCipherMigrationHook(): SQLiteDatabaseHook {
|
||||
return object : SQLiteDatabaseHook {
|
||||
private fun getCipherMigrationHook(): SQLiteDatabaseHook =
|
||||
object : SQLiteDatabaseHook {
|
||||
override fun preKey(database: SQLiteDatabase) {
|
||||
// unused atm
|
||||
}
|
||||
@ -140,6 +141,5 @@ abstract class TalkDatabase : RoomDatabase() {
|
||||
Log.i(TAG, "DB cipher_migrate END")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.extensions
|
||||
|
||||
fun Long.toIntOrZero(): Int {
|
||||
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {
|
||||
toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
@ -192,7 +192,6 @@ public class AccountRemovalWorker extends Worker {
|
||||
if (user.getId() != null) {
|
||||
String username = user.getUsername();
|
||||
try {
|
||||
appPreferences.deleteAllMessageQueuesFor(user.getUserId());
|
||||
userManager.deleteUser(user.getId());
|
||||
Log.d(TAG, "deleted user: " + username);
|
||||
} catch (Throwable e) {
|
||||
|
@ -42,5 +42,7 @@ data class ChatMessageJson(
|
||||
@JsonField(name = ["lastEditActorId"]) var lastEditActorId: String? = null,
|
||||
@JsonField(name = ["lastEditActorType"]) var lastEditActorType: String? = null,
|
||||
@JsonField(name = ["lastEditTimestamp"]) var lastEditTimestamp: Long? = 0,
|
||||
@JsonField(name = ["deleted"]) var deleted: Boolean = false
|
||||
@JsonField(name = ["deleted"]) var deleted: Boolean = false,
|
||||
@JsonField(name = ["referenceId"]) var referenceId: String? = null,
|
||||
@JsonField(name = ["silent"]) var silent: Boolean = false
|
||||
) : Parcelable
|
||||
|
@ -23,13 +23,14 @@ import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.NotificationUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
|
||||
import com.nextcloud.talk.utils.message.SendMessageUtils
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
@ -71,24 +72,31 @@ class DirectReplyReceiver : BroadcastReceiver() {
|
||||
sendDirectReply()
|
||||
}
|
||||
|
||||
private fun getMessageText(intent: Intent): CharSequence? {
|
||||
return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY)
|
||||
}
|
||||
private fun getMessageText(intent: Intent): CharSequence? =
|
||||
RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY)
|
||||
|
||||
private fun sendDirectReply() {
|
||||
val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
|
||||
val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1))
|
||||
val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl!!, roomToken!!)
|
||||
|
||||
ncApi.sendChatMessage(credentials, url, replyMessage, currentUser.displayName, null, false)
|
||||
ncApi.sendChatMessage(
|
||||
credentials,
|
||||
url,
|
||||
replyMessage,
|
||||
currentUser.displayName,
|
||||
null,
|
||||
false,
|
||||
SendMessageUtils().generateReferenceId()
|
||||
)
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(object : Observer<GenericOverall> {
|
||||
?.subscribe(object : Observer<ChatOverallSingleMessage> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun onNext(genericOverall: GenericOverall) {
|
||||
override fun onNext(message: ChatOverallSingleMessage) {
|
||||
confirmReplySent()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.ui.dialog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.data.network.NetworkMonitor
|
||||
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class TempMessageActionsDialog(
|
||||
private val chatActivity: ChatActivity,
|
||||
private val message: ChatMessage
|
||||
) : BottomSheetDialog(chatActivity) {
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dateUtils: DateUtils
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
private lateinit var binding: DialogTempMessageActionsBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
|
||||
|
||||
binding = DialogTempMessageActionsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
viewThemeUtils.material.colorBottomSheetBackground(binding.root)
|
||||
viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle)
|
||||
initMenuItems()
|
||||
}
|
||||
|
||||
private fun initMenuItems() {
|
||||
this.lifecycleScope.launch {
|
||||
val isOnline = networkMonitor.isOnline.first()
|
||||
initResendMessage(message.sendingFailed && isOnline)
|
||||
initMenuEditMessage(message.sendingFailed || !isOnline)
|
||||
initMenuDeleteMessage(message.sendingFailed || !isOnline)
|
||||
initMenuItemCopy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
private fun initResendMessage(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.menuResendMessage.setOnClickListener {
|
||||
chatActivity.chatViewModel.resendMessage(
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser!!.baseUrl!!,
|
||||
chatActivity.roomToken
|
||||
),
|
||||
message
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
binding.menuResendMessage.visibility = getVisibility(visible)
|
||||
}
|
||||
|
||||
private fun initMenuDeleteMessage(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.menuDeleteMessage.setOnClickListener {
|
||||
chatActivity.chatViewModel.deleteTempMessage(message)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
binding.menuDeleteMessage.visibility = getVisibility(visible)
|
||||
}
|
||||
|
||||
private fun initMenuEditMessage(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.menuEditMessage.setOnClickListener {
|
||||
chatActivity.messageInputViewModel.edit(message)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
binding.menuEditMessage.visibility = getVisibility(visible)
|
||||
}
|
||||
|
||||
private fun initMenuItemCopy() {
|
||||
binding.menuCopyMessage.setOnClickListener {
|
||||
chatActivity.copyMessage(message)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVisibility(visible: Boolean): Int {
|
||||
return if (visible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = TempMessageActionsDialog::class.java.simpleName
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.talk.utils.message
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
|
||||
class SendMessageUtils {
|
||||
fun generateReferenceId(): String {
|
||||
val randomString = UUID.randomUUID().toString()
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hashBytes = digest.digest(randomString.toByteArray(Charsets.UTF_8))
|
||||
return hashBytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun removeYearFromTimestamp(timestampMillis: Long): Int {
|
||||
val calendar = Calendar.getInstance().apply { timeInMillis = timestampMillis }
|
||||
|
||||
val month = calendar.get(Calendar.MONTH)
|
||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
val hour = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
val minute = calendar.get(Calendar.MINUTE)
|
||||
val second = calendar.get(Calendar.SECOND)
|
||||
return (month * 1000000) + (day * 10000) + (hour * 100) + (minute * 10) + second
|
||||
}
|
||||
}
|
@ -175,12 +175,6 @@ public interface AppPreferences {
|
||||
|
||||
int getLastKnownId(String internalConversationId, int defaultValue);
|
||||
|
||||
void saveMessageQueue(String internalConversationId, List<MessageInputViewModel.QueuedMessage> queue);
|
||||
|
||||
List<MessageInputViewModel.QueuedMessage> getMessageQueue(String internalConversationId);
|
||||
|
||||
void deleteAllMessageQueuesFor(String userId);
|
||||
|
||||
void saveVoiceMessagePlaybackSpeedPreferences(Map<String, PlaybackSpeed> speeds);
|
||||
|
||||
Map<String, PlaybackSpeed> readVoiceMessagePlaybackSpeedPreferences();
|
||||
|
@ -17,7 +17,6 @@ import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
|
||||
import com.nextcloud.talk.ui.PlaybackSpeed
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
@ -501,76 +500,6 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
||||
return if (lastReadId.isNotEmpty()) lastReadId.toInt() else defaultValue
|
||||
}
|
||||
|
||||
override fun saveMessageQueue(
|
||||
internalConversationId: String,
|
||||
queue: MutableList<MessageInputViewModel.QueuedMessage>?
|
||||
) {
|
||||
runBlocking<Unit> {
|
||||
async {
|
||||
var queueStr = ""
|
||||
queue?.let {
|
||||
for (msg in queue) {
|
||||
val msgStr = "${msg.id},${msg.message},${msg.replyTo},${msg.displayName},${
|
||||
msg
|
||||
.sendWithoutNotification
|
||||
}^"
|
||||
queueStr += msgStr
|
||||
}
|
||||
}
|
||||
writeString(internalConversationId + MESSAGE_QUEUE, queueStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||
override fun getMessageQueue(internalConversationId: String): MutableList<MessageInputViewModel.QueuedMessage> {
|
||||
val queueStr =
|
||||
runBlocking { async { readString(internalConversationId + MESSAGE_QUEUE).first() } }.getCompleted()
|
||||
|
||||
val queue: MutableList<MessageInputViewModel.QueuedMessage> = mutableListOf()
|
||||
if (queueStr.isEmpty()) return queue
|
||||
|
||||
for (msgStr in queueStr.split("^")) {
|
||||
try {
|
||||
if (msgStr.isNotEmpty()) {
|
||||
val msgArray = msgStr.split(",")
|
||||
val id = msgArray[ID].toInt()
|
||||
val message = msgArray[MESSAGE_INDEX]
|
||||
val replyTo = msgArray[REPLY_TO_INDEX].toInt()
|
||||
val displayName = msgArray[DISPLAY_NAME_INDEX]
|
||||
val silent = msgArray[SILENT_INDEX].toBoolean()
|
||||
|
||||
val qMsg = MessageInputViewModel.QueuedMessage(id, message, displayName, replyTo, silent)
|
||||
queue.add(qMsg)
|
||||
}
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
Log.e(TAG, "Message string: $msgStr\n Queue String: $queueStr \n$e")
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
override fun deleteAllMessageQueuesFor(userId: String) {
|
||||
runBlocking {
|
||||
async {
|
||||
val keyList = mutableListOf<Preferences.Key<*>>()
|
||||
val preferencesMap = context.dataStore.data.first().asMap()
|
||||
for (preference in preferencesMap) {
|
||||
if (preference.key.name.contains("$userId@")) {
|
||||
keyList.add(preference.key)
|
||||
}
|
||||
}
|
||||
|
||||
for (key in keyList) {
|
||||
context.dataStore.edit {
|
||||
it.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveVoiceMessagePlaybackSpeedPreferences(speeds: Map<String, PlaybackSpeed>) {
|
||||
Json.encodeToString(speeds).let {
|
||||
runBlocking<Unit> { async { writeString(VOICE_MESSAGE_PLAYBACK_SPEEDS, it) } }
|
||||
@ -655,13 +584,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
private val TAG = AppPreferencesImpl::class.simpleName
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
private const val ID: Int = 0
|
||||
private const val MESSAGE_INDEX: Int = 1
|
||||
private const val REPLY_TO_INDEX: Int = 2
|
||||
private const val DISPLAY_NAME_INDEX: Int = 3
|
||||
private const val SILENT_INDEX: Int = 4
|
||||
const val PROXY_TYPE = "proxy_type"
|
||||
const val PROXY_SERVER = "proxy_server"
|
||||
const val PROXY_HOST = "proxy_host"
|
||||
const val PROXY_PORT = "proxy_port"
|
||||
const val PROXY_CRED = "proxy_credentials"
|
||||
@ -686,7 +609,6 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
||||
const val DB_ROOM_MIGRATED = "db_room_migrated"
|
||||
const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run"
|
||||
const val TYPING_STATUS = "typing_status"
|
||||
const val MESSAGE_QUEUE = "@message_queue"
|
||||
const val VOICE_MESSAGE_PLAYBACK_SPEEDS = "voice_message_playback_speeds"
|
||||
const val SHOW_REGULAR_NOTIFICATION_WARNING = "show_regular_notification_warning"
|
||||
const val LAST_NOTIFICATION_WARNING = "last_notification_warning"
|
||||
|
16
app/src/main/res/drawable/baseline_error_outline_24.xml
Normal file
16
app/src/main/res/drawable/baseline_error_outline_24.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2025 Google LLC
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
</vector>
|
16
app/src/main/res/drawable/baseline_report_problem_24.xml
Normal file
16
app/src/main/res/drawable/baseline_report_problem_24.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2021-2024 Google LLC
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
19
app/src/main/res/drawable/baseline_schedule_24.xml
Normal file
19
app/src/main/res/drawable/baseline_schedule_24.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2025 Google LLC
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
|
||||
</vector>
|
165
app/src/main/res/layout/dialog_temp_message_actions.xml
Normal file
165
app/src/main/res/layout/dialog_temp_message_actions.xml
Normal file
@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<LinearLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/standard_half_padding">
|
||||
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/bottom_sheet_drag_handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/message_actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_resend_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu_icon_resend_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:src="@drawable/ic_send"
|
||||
app:tint="@color/high_emphasis_menu_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/menu_text_resend_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/standard_double_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
android:text="@string/resend_message"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/high_emphasis_text"
|
||||
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_copy_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu_icon_copy_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:src="@drawable/ic_content_copy"
|
||||
app:tint="@color/high_emphasis_menu_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/menu_text_copy_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/standard_double_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
android:text="@string/nc_copy_message"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/high_emphasis_text"
|
||||
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_edit_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu_icon_edit_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/edit_message_icon_description"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:src="@drawable/ic_edit_24"
|
||||
app:tint="@color/high_emphasis_menu_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/menu_text_edit_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/standard_double_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
android:text="@string/nc_edit_message"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/high_emphasis_text"
|
||||
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_delete_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu_icon_delete_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:src="@drawable/ic_delete"
|
||||
app:tint="@color/high_emphasis_menu_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/menu_text_delete_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/standard_double_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
android:text="@string/nc_delete_message"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/high_emphasis_text"
|
||||
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
@ -43,7 +43,6 @@
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Talk to you later!" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@id/messageTime"
|
||||
android:layout_width="wrap_content"
|
||||
@ -76,13 +75,36 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkMark"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="@dimen/message_bubble_checkmark_height"
|
||||
android:layout_below="@id/messageTime"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@null"
|
||||
app:layout_alignSelf="center"
|
||||
app:tint="@color/high_emphasis_text"
|
||||
tools:src="@drawable/ic_check_all" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sending_failed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/message_bubble_checkmark_height"
|
||||
android:layout_below="@id/messageTime"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@null"
|
||||
app:layout_alignSelf="center"
|
||||
app:tint="@color/high_emphasis_text" />
|
||||
app:tint="@color/high_emphasis_text"
|
||||
tools:src="@drawable/ic_warning_white"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sending_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/message_bubble_checkmark_height"
|
||||
android:layout_below="@id/messageTime"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@null"
|
||||
app:layout_alignSelf="center"
|
||||
app:tint="@color/high_emphasis_text"
|
||||
tools:src="@drawable/baseline_schedule_24"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/reactions"
|
||||
|
@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<RelativeLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_centerVertical="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/temp_msg_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:paddingHorizontal="@dimen/standard_half_padding"
|
||||
android:layout_marginEnd="@dimen/standard_quarter_margin" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/temp_msg_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:paddingHorizontal="@dimen/standard_half_padding"
|
||||
android:layout_marginStart="@dimen/standard_quarter_margin" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@id/bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
|
||||
app:alignContent="stretch"
|
||||
app:alignItems="stretch"
|
||||
app:flexWrap="wrap"
|
||||
app:justifyContent="flex_end">
|
||||
|
||||
<include
|
||||
android:id="@+id/message_quote"
|
||||
layout="@layout/item_message_quote"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<androidx.emoji2.widget.EmojiTextView
|
||||
android:id="@id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColorHighlight="@color/nc_grey"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Talk to you later!" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/message_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
</RelativeLayout>
|
@ -433,6 +433,9 @@ How to translate with transifex:
|
||||
<string name="nc_formatted_message_you">You: %1$s</string>
|
||||
<string name="nc_message_read">Message read</string>
|
||||
<string name="nc_message_sent">Message sent</string>
|
||||
<string name="nc_message_offline">Offline</string>
|
||||
<string name="nc_message_failed">Failed</string>
|
||||
<string name="nc_message_sending">Sending</string>
|
||||
<string name="nc_message_failed_to_send">Failed to send message:</string>
|
||||
<string name="nc_remote_audio_off">Remote audio off</string>
|
||||
<string name="nc_add_attachment">Add attachment</string>
|
||||
@ -820,7 +823,6 @@ How to translate with transifex:
|
||||
<string name="show_banned_participants">Show banned participants</string>
|
||||
<string name="bans_list">Bans list</string>
|
||||
<string name="connection_lost_sent_messages_are_queued">Connection lost - Sent messages are queued</string>
|
||||
<string name="connection_lost_queued">Connection lost - %1$d are queued</string>
|
||||
<string name="connection_established">Connection established</string>
|
||||
<string name="message_deleted_by_you">Message deleted by you</string>
|
||||
<string name="unban">Unban</string>
|
||||
@ -843,4 +845,5 @@ How to translate with transifex:
|
||||
<string name="user_absence">%1$s is out of office and might not respond</string>
|
||||
<string name="user_absence_for_one_day">%1$s is out of office today</string>
|
||||
<string name="user_absence_replacement">Replacement: </string>
|
||||
<string name="resend_message">Resend</string>
|
||||
</resources>
|
||||
|
@ -1,2 +1,2 @@
|
||||
DO NOT TOUCH; GENERATED BY DRONE
|
||||
<span class="mdl-layout-title">Lint Report: 36 errors and 106 warnings</span>
|
||||
<span class="mdl-layout-title">Lint Report: 36 errors and 104 warnings</span>
|
||||
|
Loading…
Reference in New Issue
Block a user