diff --git a/app/build.gradle b/app/build.gradle index 4f63e86d..c258dced 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,10 @@ android { testInstrumentationRunner "org.pixeldroid.app.testUtility.TestRunner" testInstrumentationRunnerArguments clearPackageData: 'true' + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } } sourceSets { main.java.srcDirs += 'src/main/java' diff --git a/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/8.json b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/8.json new file mode 100644 index 00000000..a0a57778 --- /dev/null +++ b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/8.json @@ -0,0 +1,985 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "37b7f1d842d148e1d117ac9caae8fb51", + "entities": [ + { + "tableName": "instances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxStatusChars", + "columnName": "maxStatusChars", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxPhotoSize", + "columnName": "maxPhotoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxVideoSize", + "columnName": "maxVideoSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "albumLimit", + "columnName": "albumLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoEnabled", + "columnName": "videoEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelfed", + "columnName": "pixelfed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "display_name", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar_static", + "columnName": "avatar_static", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_users_instance_uri", + "unique": false, + "columnNames": [ + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "instances", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instance_uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "homePosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_homePosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "publicPosts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spoiler_text", + "columnName": "spoiler_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media_attachments", + "columnName": "media_attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogs_count", + "columnName": "reblogs_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favourites_count", + "columnName": "favourites_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replies_count", + "columnName": "replies_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_id", + "columnName": "in_reply_to_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "in_reply_to_account", + "columnName": "in_reply_to_account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblog", + "columnName": "reblog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_publicPosts_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_notifications_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "tabsChecked", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checked", + "columnName": "checked", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "index", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_tabsChecked_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "directMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `unread` INTEGER, `accounts` TEXT, `last_status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_status", + "columnName": "last_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_directMessages_user_id_instance_uri", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_directMessages_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + }, + { + "tableName": "directMessagesThreads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `hidden` INTEGER, `isAuthor` INTEGER, `type` TEXT, `text` TEXT, `media` TEXT, `carousel` TEXT, `created_at` TEXT, `timeAgo` TEXT, `reportId` TEXT, `conversationsId` TEXT NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `conversationsId`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAuthor", + "columnName": "isAuthor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "media", + "columnName": "media", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "carousel", + "columnName": "carousel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created_at", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeAgo", + "columnName": "timeAgo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reportId", + "columnName": "reportId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conversationsId", + "columnName": "conversationsId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "user_id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instance_uri", + "columnName": "instance_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "conversationsId", + "user_id", + "instance_uri" + ] + }, + "indices": [ + { + "name": "index_directMessagesThreads_user_id_instance_uri_conversationsId", + "unique": false, + "columnNames": [ + "user_id", + "instance_uri", + "conversationsId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_directMessagesThreads_user_id_instance_uri_conversationsId` ON `${TABLE_NAME}` (`user_id`, `instance_uri`, `conversationsId`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id", + "instance_uri" + ], + "referencedColumns": [ + "user_id", + "instance_uri" + ] + } + ] + } + ], + "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, '37b7f1d842d148e1d117ac9caae8fb51')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5608ec5..8e9b54b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -141,6 +141,12 @@ + + + Unit, val context: Context) : RecyclerView.EdgeEffectFactory() { + override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect { + + return object : EdgeEffect(recyclerView.context) { + + // A reference to the [SpringAnimation] for this RecyclerView used to bring the item back after the over-scroll effect. + var translationAnim: SpringAnimation? = null + + override fun onPull(deltaDistance: Float) { + super.onPull(deltaDistance) + handlePull(deltaDistance) + } + + override fun onPull(deltaDistance: Float, displacement: Float) { + super.onPull(deltaDistance, displacement) + handlePull(deltaDistance) + } + + private fun handlePull(deltaDistance: Float) { + // This is called on every touch event while the list is scrolled with a finger. + + // Translate the recyclerView with the distance + val sign = if (direction == DIRECTION_BOTTOM) -1 else 1 + val translationYDelta = sign * recyclerView.width * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE + recyclerView.translationY += translationYDelta + + translationAnim?.cancel() + } + + override fun onRelease() { + super.onRelease() + // The finger is lifted. Start the animation to bring translation back to the resting state. + if (recyclerView.translationY != 0f) { + if (direction == DIRECTION_BOTTOM && recyclerView.translationY.toInt().absoluteValue.pxToDp(context) > 50) { + refreshCallback() + } + + translationAnim = createAnim()?.also { it.start() } + } + } + + override fun onAbsorb(velocity: Int) { + super.onAbsorb(velocity) + + // The list has reached the edge on fling. + val sign = if (direction == DIRECTION_BOTTOM) -1 else 1 + + val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE + translationAnim?.cancel() + translationAnim = createAnim().setStartVelocity(translationVelocity)?.also { it.start() } + } + + override fun draw(canvas: Canvas?): Boolean { + // don't paint the usual edge effect + return false + } + + override fun isFinished(): Boolean { + // Without this, will skip future calls to onAbsorb() + return translationAnim?.isRunning?.not() ?: true + } + + private fun createAnim() = SpringAnimation(recyclerView, SpringAnimation.TRANSLATION_Y) + .setSpring( + SpringForce() + .setFinalPosition(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/ConversationActivity.kt b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationActivity.kt new file mode 100644 index 00000000..29f8415e --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationActivity.kt @@ -0,0 +1,109 @@ +package org.pixeldroid.app.directmessages + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.ActivityConversationBinding +import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID +import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID +import org.pixeldroid.app.utils.BaseActivity +import org.pixeldroid.app.utils.api.PixelfedAPI + +class ConversationActivity : BaseActivity() { + lateinit var binding: ActivityConversationBinding + + private lateinit var conversationFragment: ConversationFragment + + companion object { + const val USERNAME = "ConversationActivityUsername" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityConversationBinding.inflate(layoutInflater) + + conversationFragment = ConversationFragment() + + setContentView(binding.root) + setSupportActionBar(binding.topBar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val userName = intent?.getSerializableExtra(USERNAME) as? String? + supportActionBar?.title = getString(R.string.dm_title, userName) + + val conversationId = intent?.getSerializableExtra(CONVERSATION_ID) as String + val pid = intent?.getSerializableExtra(PROFILE_ID) as String + + activateCommenter(pid) + + initConversationFragment(pid, conversationId, savedInstanceState) + } + + private fun activateCommenter(pid: String) { + //Activate commenter + binding.submitComment.setOnClickListener { + val textIn = binding.editComment.text + //Open text input + if(textIn.isNullOrEmpty()) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.empty_comment), + Toast.LENGTH_SHORT + ).show() + } else { + //Post the comment + lifecycleScope.launchWhenCreated { + apiHolder.api?.let { it1 -> sendMessage(it1, pid) } + } + } + } + } + + private fun initConversationFragment(profileId: String, conversationId: String, savedInstanceState: Bundle?) { + + val arguments = Bundle() + arguments.putSerializable(CONVERSATION_ID, conversationId) + arguments.putSerializable(PROFILE_ID, profileId) + conversationFragment.arguments = arguments + + //TODO finish work here! commentFragment needs the swiperefreshlayout.. how?? + //Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research + if (savedInstanceState == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.conversationFragment, conversationFragment) + } + } + } + + private suspend fun sendMessage( + api: PixelfedAPI, + pid: String, + ) { + val textIn = binding.editComment.text + val nonNullText = textIn.toString() + try { + binding.submitComment.isEnabled = false + binding.editComment.isEnabled = false + api.sendDirectMessage(pid, nonNullText) + + //Reload to add the comment to the comment section + conversationFragment.adapter.refresh() + + binding.editComment.isEnabled = true + binding.editComment.text = null + binding.submitComment.isEnabled = true + } catch (exception: Exception) { + Log.e("DM SEND ERROR", exception.toString()) + Toast.makeText( + binding.root.context, binding.root.context.getString(R.string.comment_error), + Toast.LENGTH_SHORT + ).show() + } + } + +} diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/ConversationFragment.kt b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationFragment.kt new file mode 100644 index 00000000..415a49d6 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationFragment.kt @@ -0,0 +1,188 @@ +package org.pixeldroid.app.directmessages + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.bumptech.glide.Glide +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.DirectMessagesConversationItemBinding +import org.pixeldroid.app.posts.AlbumActivity +import org.pixeldroid.app.posts.AlbumViewModel +import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment +import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedContentRepository +import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel +import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory +import org.pixeldroid.app.utils.api.objects.Conversation +import org.pixeldroid.app.utils.api.objects.Message +import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity +import org.pixeldroid.app.utils.di.PixelfedAPIHolder + + +/** + * Fragment for one Direct Messages conversation + */ +class ConversationFragment : CachedFeedFragment() { + + companion object { + const val CONVERSATION_ID = "ConversationFragmentConversationId" + const val PROFILE_ID = "ConversationFragmentProfileId" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + adapter = DirectMessagesListAdapter(apiHolder) + } + + @OptIn(ExperimentalPagingApi::class) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = super.createView(inflater, container, savedInstanceState, true) + + val pid = arguments?.getSerializable(PROFILE_ID) as String + val conversationId = arguments?.getSerializable(CONVERSATION_ID) as String + + val dao = db.directMessagesConversationDao() + val remoteMediator = ConversationRemoteMediator(apiHolder, db, pid, conversationId) + // get the view model + @Suppress("UNCHECKED_CAST") + viewModel = ViewModelProvider( + requireActivity(), + ViewModelFactory( + db, dao, remoteMediator, + FeedContentRepository(db, dao, remoteMediator, conversationId) + ) + )["directMessagesConversation", FeedViewModel::class.java] as FeedViewModel + + launch() + initSearch() + + return view + } + + /** + * View Holder for a [Conversation] RecyclerView list item. + */ + class DirectMessagesConversationViewHolder(val binding: DirectMessagesConversationItemBinding) : RecyclerView.ViewHolder(binding.root) { + private var message: DirectMessageDatabaseEntity? = null + + init { + itemView.setOnClickListener { + message?.let { + if (it.type == "photo") { + val intent = Intent(itemView.context, AlbumActivity::class.java) + + intent.putExtra(AlbumViewModel.ALBUM_IMAGES, ArrayList(it.carousel.orEmpty())) + intent.putExtra(AlbumViewModel.ALBUM_INDEX, 0) + + itemView.context.startActivity(intent) + } + } + } + } + + fun bind( + message: DirectMessageDatabaseEntity?, + api: PixelfedAPIHolder, + lifecycleScope: LifecycleCoroutineScope, + ) { + this.message = message + + if(message?.isAuthor == true) { + binding.messageIncoming.visibility = GONE + binding.messageOutgoing.visibility = VISIBLE + binding.textMessageOutgoing.text = message.text + } else { + binding.messageIncoming.visibility = VISIBLE + binding.messageOutgoing.visibility = GONE + binding.textMessageIncoming.text = message?.text ?: "" + } + + if (message?.type == "photo"){ + binding.imageMessageIncoming.visibility = VISIBLE + binding.imageMessageOutgoing.visibility = VISIBLE + binding.textMessageOutgoing.visibility = GONE + binding.textMessageOutgoing.visibility = GONE + Glide.with(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming) + .load(message.media) + .into(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming) + } else { + binding.imageMessageIncoming.visibility = GONE + binding.imageMessageOutgoing.visibility = GONE + binding.textMessageOutgoing.visibility = VISIBLE + binding.textMessageIncoming.visibility = VISIBLE + } + + message?.created_at.let { +// if (it == null) binding.messageTime.text = "" +// else setTextViewFromISO8601( +// it, +// binding.messageTime, +// false +// ) + } + } + + companion object { + fun create(parent: ViewGroup): DirectMessagesConversationViewHolder { + val itemBinding = DirectMessagesConversationItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return DirectMessagesConversationViewHolder(itemBinding) + } + } + } + + + inner class DirectMessagesListAdapter( + private val apiHolder: PixelfedAPIHolder, + ) : PagingDataAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: DirectMessageDatabaseEntity, + newItem: DirectMessageDatabaseEntity + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: DirectMessageDatabaseEntity, + newItem: DirectMessageDatabaseEntity + ): Boolean = + oldItem == newItem + } + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return DirectMessagesConversationViewHolder.create(parent) + } + + override fun getItemViewType(position: Int): Int { + return R.layout.direct_messages_conversation_item + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val uiModel = getItem(position) + uiModel?.let { + (holder as DirectMessagesConversationViewHolder).bind( + it, + apiHolder, + lifecycleScope + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/ConversationRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationRemoteMediator.kt new file mode 100644 index 00000000..def077d4 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/ConversationRemoteMediator.kt @@ -0,0 +1,74 @@ +package org.pixeldroid.app.directmessages + +import android.util.Log +import androidx.paging.* +import androidx.room.withTransaction +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity +import java.lang.Exception +import java.lang.NullPointerException +import javax.inject.Inject + +/** + * RemoteMediator for a Direct Messages conversation. + * + * A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote + * source into a local source wrapped by a [PagingSource], e.g., loading data from network into + * a local db cache. + */ +@OptIn(ExperimentalPagingApi::class) +class ConversationRemoteMediator @Inject constructor( + private val apiHolder: PixelfedAPIHolder, + private val db: AppDatabase, + private val pid: String, + private val conversationId: String +) : RemoteMediator() { + + override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult { + try { + val user = db.userDao().getActiveUser() + ?: return MediatorResult.Error(NullPointerException("No active user exists")) + + val nextPage = when (loadType) { + LoadType.REFRESH -> null + LoadType.PREPEND -> { + // No prepend for the moment, might be nice to add later + db.directMessagesConversationDao().lastMessageId(user.user_id, user.instance_uri, conversationId) + ?: return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> + return MediatorResult.Success(endOfPaginationReached = true) + } + + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + val apiResponse = + api.directMessagesConversation( + pid = pid, + max_id = nextPage, + ) + //TODO prepend + + val messages = apiResponse.messages.map { + DirectMessageDatabaseEntity( + it, + conversationId, + user + ) + } + + val endOfPaginationReached = messages.isEmpty() + + db.withTransaction { + // Clear table in the database + if (loadType == LoadType.REFRESH) { + db.directMessagesConversationDao().clearFeedContent(user.user_id, user.instance_uri, conversationId) + } + db.directMessagesConversationDao().insertAll(messages) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: Exception){ + return MediatorResult.Error(exception) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesActivity.kt b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesActivity.kt new file mode 100644 index 00000000..96d576f9 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesActivity.kt @@ -0,0 +1,40 @@ +package org.pixeldroid.app.directmessages + +import android.os.Bundle +import androidx.fragment.app.commit +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.ActivityConversationBinding +import org.pixeldroid.app.databinding.ActivityFollowersBinding +import org.pixeldroid.app.profile.FollowsActivity +import org.pixeldroid.app.utils.BaseActivity + +class DirectMessagesActivity : BaseActivity() { + lateinit var binding: ActivityFollowersBinding + + private lateinit var conversationFragment: DirectMessagesFragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityFollowersBinding.inflate(layoutInflater) + + conversationFragment = DirectMessagesFragment() + + setContentView(binding.root) + setSupportActionBar(binding.topBar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setTitle(R.string.direct_messages) + + initConversationFragment(savedInstanceState) + } + + private fun initConversationFragment(savedInstanceState: Bundle?) { + //TODO finish work here! commentFragment needs the swiperefreshlayout.. how?? + //Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research + if (savedInstanceState == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.conversationFragment, conversationFragment) + } + } + } +} diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesFragment.kt b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesFragment.kt new file mode 100644 index 00000000..48d2ab47 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesFragment.kt @@ -0,0 +1,171 @@ +package org.pixeldroid.app.directmessages + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.DirectMessagesListItemBinding +import org.pixeldroid.app.directmessages.ConversationActivity.Companion.USERNAME +import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID +import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID +import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment +import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel +import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory +import org.pixeldroid.app.posts.parseHTMLText +import org.pixeldroid.app.posts.setTextViewFromISO8601 +import org.pixeldroid.app.profile.ProfileActivity +import org.pixeldroid.app.utils.api.objects.Account +import org.pixeldroid.app.utils.api.objects.Conversation +import org.pixeldroid.app.utils.di.PixelfedAPIHolder + + +/** + * Fragment for the list of Direct Messages conversations. + */ +class DirectMessagesFragment : CachedFeedFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + adapter = DirectMessagesListAdapter(apiHolder) + } + + @OptIn(ExperimentalPagingApi::class) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + + // get the view model + @Suppress("UNCHECKED_CAST") + viewModel = ViewModelProvider( + requireActivity(), + ViewModelFactory(db, db.directMessagesDao(), DirectMessagesRemoteMediator(apiHolder, db)) + )["directMessagesList", FeedViewModel::class.java] as FeedViewModel + + launch() + initSearch() + + return view + } + + /** + * View Holder for a [Conversation] RecyclerView list item. + */ + class DirectMessagesListViewHolder(val binding: DirectMessagesListItemBinding) : RecyclerView.ViewHolder(binding.root) { + private var conversation: Conversation? = null + + init { + itemView.setOnClickListener { + conversation?.accounts?.firstOrNull()?.let { + val intent = Intent(itemView.context, ConversationActivity::class.java).apply { + putExtra(PROFILE_ID, it.id) + putExtra(CONVERSATION_ID, conversation?.id) + putExtra(USERNAME, it.getDisplayName()) + } + itemView.context.startActivity(intent) + } + } + binding.dmAvatar.setOnClickListener { + conversation?.accounts?.firstOrNull()?.let { + val intent = Intent(itemView.context, ProfileActivity::class.java).apply { + putExtra(Account.ACCOUNT_TAG, it) + } + itemView.context.startActivity(intent) + } + } + } + + fun bind( + conversation: Conversation?, + api: PixelfedAPIHolder, + lifecycleScope: LifecycleCoroutineScope, + ) { + + this.conversation = conversation + + val account = conversation?.accounts?.firstOrNull() + + Glide.with(itemView).load(account?.anyAvatar()).circleCrop() + .into(binding.dmAvatar) + + binding.dmUsername.text = account?.getDisplayName() + + binding.dmLastMessage.text = parseHTMLText( + conversation?.last_status?.content ?: "", + conversation?.last_status?.mentions, + api, + itemView.context, + lifecycleScope + ) + + conversation?.last_status?.created_at.let { + if (it == null) binding.messageTime.text = "" + else setTextViewFromISO8601( + it, + binding.messageTime, + false + ) + } + } + + companion object { + fun create(parent: ViewGroup): DirectMessagesListViewHolder { + val itemBinding = DirectMessagesListItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return DirectMessagesListViewHolder(itemBinding) + } + } + } + + + inner class DirectMessagesListAdapter( + private val apiHolder: PixelfedAPIHolder, + ) : PagingDataAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Conversation, + newItem: Conversation + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: Conversation, + newItem: Conversation + ): Boolean = + oldItem == newItem + } + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return DirectMessagesListViewHolder.create(parent) + } + + override fun getItemViewType(position: Int): Int { + return R.layout.direct_messages_list_item + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val uiModel = getItem(position) + uiModel?.let { + (holder as DirectMessagesListViewHolder).bind( + it, + apiHolder, + lifecycleScope + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesRemoteMediator.kt new file mode 100644 index 00000000..dd99dc03 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/directmessages/DirectMessagesRemoteMediator.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.pixeldroid.app.directmessages + +import androidx.paging.* +import androidx.paging.PagingSource.LoadResult +import androidx.room.withTransaction +import org.pixeldroid.app.utils.api.objects.Conversation +import org.pixeldroid.app.utils.db.AppDatabase +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import org.pixeldroid.app.utils.api.objects.Notification +import retrofit2.HttpException +import java.lang.Exception +import java.lang.NullPointerException +import javax.inject.Inject + +/** + * RemoteMediator for the notifications. + * + * A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote + * source into a local source wrapped by a [PagingSource], e.g., loading data from network into + * a local db cache. + */ +@OptIn(ExperimentalPagingApi::class) +class DirectMessagesRemoteMediator @Inject constructor( + private val apiHolder: PixelfedAPIHolder, + private val db: AppDatabase +) : RemoteMediator() { + + override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult { + try { + val user = db.userDao().getActiveUser() + ?: return MediatorResult.Error(NullPointerException("No active user exists")) + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + + val nextPage = when (loadType) { + LoadType.REFRESH -> null + LoadType.PREPEND -> { + // No prepend for the moment, might be nice to add later + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> state.lastItemOrNull()?.id?.toIntOrNull() + ?.let { state.closestPageToPosition(it) }?.nextKey + ?: return MediatorResult.Success(endOfPaginationReached = true) + } + + val apiResponse = + // Pixelfed uses Laravel's paging mechanism for pagination. + //TODO, implement also for Mastodon (see FollowersPagingSource) + api.directMessagesList( + limit = state.config.pageSize.toString(), + page = nextPage + ) + + apiResponse.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri} + + val endOfPaginationReached = apiResponse.isEmpty() + + db.withTransaction { + // Clear table in the database + if (loadType == LoadType.REFRESH) { + db.directMessagesDao().clearFeedContent(user.user_id, user.instance_uri) + } + db.directMessagesDao().insertAll(apiResponse) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: Exception){ + return MediatorResult.Error(exception) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt index 9f474460..31592d64 100644 --- a/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt @@ -24,24 +24,16 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.GravityCompat import androidx.core.view.children import androidx.core.view.isVisible -import androidx.core.view.children -import androidx.core.view.get -import androidx.core.view.isVisible -import androidx.core.view.marginEnd -import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.paging.ExperimentalPagingApi -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.bumptech.glide.Glide -import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.color.DynamicColors import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationView @@ -60,9 +52,11 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.widget.AccountHeaderView import kotlinx.coroutines.launch import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist -import org.pixeldroid.app.login.LoginActivity import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityMainBinding +import org.pixeldroid.app.directmessages.DirectMessagesActivity +import org.pixeldroid.app.directmessages.DirectMessagesFragment +import org.pixeldroid.app.login.LoginActivity import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.posts.NestedScrollableHost import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment @@ -79,7 +73,6 @@ import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.updateUserInfoDb import org.pixeldroid.app.utils.hasInternet -import org.pixeldroid.app.utils.loadDefaultMenuTabs import org.pixeldroid.app.utils.loadDbMenuTabs import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG @@ -260,6 +253,10 @@ class MainActivity : BaseActivity() { getUpdatedAccount() binding.drawer?.itemAdapter?.add( + primaryDrawerItem { + nameRes = R.string.direct_messages + iconRes = R.drawable.message + }, primaryDrawerItem { nameRes = R.string.menu_account iconRes = R.drawable.person @@ -276,9 +273,10 @@ class MainActivity : BaseActivity() { binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position -> when (position) { - 1 -> launchActivity(ProfileActivity()) - 2 -> launchActivity(SettingsActivity()) - 3 -> logOut() + 1 -> launchActivity(DirectMessagesActivity()) + 2 -> launchActivity(ProfileActivity()) + 3 -> launchActivity(SettingsActivity()) + 4 -> logOut() } false } @@ -458,9 +456,7 @@ class MainActivity : BaseActivity() { @OptIn(ExperimentalPagingApi::class) private fun setupTabs() { - val tabsCheckedDbEntry = with (db.userDao().getActiveUser()!!) { - db.tabsDao().getTabsChecked(user_id, instance_uri) - } + val tabsCheckedDbEntry = db.tabsDao().getTabsChecked(user!!.user_id, user!!.instance_uri) val pageIds = listOf(R.id.page_1, R.id.page_2, R.id.page_3, R.id.page_4, R.id.page_5) fun Tab.getFragment(): (() -> Fragment) { @@ -480,33 +476,34 @@ class MainActivity : BaseActivity() { arguments = Bundle().apply { putBoolean("home", false) } } } } + Tab.DIRECT_MESSAGES -> {{ + DirectMessagesFragment() + }} } } val tabs = if (tabsCheckedDbEntry.isEmpty()) { - // Load default menu - loadDefaultMenuTabs(applicationContext, binding.root) + // Default menu + Tab.defaultTabs } else { // Get current menu visibility and order from settings - val tabsChecked = loadDbMenuTabs(applicationContext, tabsCheckedDbEntry).filter { it.second }.map { it.first } + loadDbMenuTabs(tabsCheckedDbEntry).filter { it.second }.map { it.first } + } - val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply { - clear() - } - ?: binding.navigation?.menu?.apply { - removeGroup(R.id.tabsId) + val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply { + clear() + } + ?: binding.navigation?.menu?.apply { + if(tabs.contains(Tab.DIRECT_MESSAGES)) removeGroup(R.id.dmNavigationGroup) } - tabsChecked.zip(pageIds).forEach { (tabId, pageId) -> - with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) { - val tabIcon = tabId.getDrawable(applicationContext) - if (tabIcon != null) { - this?.icon = tabIcon - } + tabs.zip(pageIds).forEach { (tabId, pageId) -> + with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) { + val tabIcon = tabId.getDrawable(applicationContext) + if (tabIcon != null) { + this?.icon = tabIcon } } - - tabsChecked } val tabArray: List<() -> Fragment> = tabs.map { it.getFragment() } @@ -558,6 +555,7 @@ class MainActivity : BaseActivity() { fun MenuItem.buttonPos() { when(itemId){ + R.id.dms -> launchActivity(DirectMessagesActivity()) R.id.my_profile -> launchActivity(ProfileActivity()) R.id.settings -> launchActivity(SettingsActivity()) R.id.log_out -> logOut() diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt index 02c2efbc..7b652101 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt @@ -50,7 +50,7 @@ private fun showError( * Makes the UI respond to various [LoadState]s, including errors when an error message is shown. */ internal fun initAdapter( - progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout, + progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout?, recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding, adapter: PagingDataAdapter, header: StoriesAdapter? = null @@ -71,7 +71,7 @@ internal fun initAdapter( ).toTypedArray() ) - swipeRefreshLayout.setOnRefreshListener { + swipeRefreshLayout?.setOnRefreshListener { adapter.refresh() adapter.notifyDataSetChanged() header?.refreshStories() @@ -79,7 +79,7 @@ internal fun initAdapter( adapter.addLoadStateListener { loadState -> - if(!progressBar.isVisible && swipeRefreshLayout.isRefreshing) { + if(!progressBar.isVisible && swipeRefreshLayout?.isRefreshing == true) { // Stop loading spinner when loading is done swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading } diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt index 3f30c74b..1229407b 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt @@ -1,22 +1,27 @@ package org.pixeldroid.app.posts.feeds.cachedFeeds import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadState.NotLoading import androidx.paging.PagingDataAdapter import androidx.paging.RemoteMediator +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch import org.pixeldroid.app.databinding.FragmentFeedBinding +import org.pixeldroid.app.directmessages.BounceEdgeEffectFactory import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.stories.StoriesAdapter import org.pixeldroid.app.utils.BaseFragment @@ -67,26 +72,57 @@ open class CachedFeedFragment : BaseFragment() { // } } - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - + fun createView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, reverseLayout: Boolean = false): ConstraintLayout { super.onCreateView(inflater, container, savedInstanceState) binding = FragmentFeedBinding.inflate(layoutInflater) - initAdapter(binding.progressBar, binding.swipeRefreshLayout, + val callback: () -> Unit = { + binding.bottomLoadingBar.visibility = View.VISIBLE + GlobalScope.launch(Dispatchers.Main) { + delay(1000) // Wait 1 second + binding.bottomLoadingBar.visibility = View.GONE + } + adapter.refresh() + adapter.notifyDataSetChanged() + } + + val swipeRefreshLayout = if(reverseLayout) { + binding.swipeRefreshLayout.isEnabled = false + binding.list.apply { + layoutManager = LinearLayoutManager(context).apply { + stackFromEnd = false + this.reverseLayout = true + } + edgeEffectFactory = BounceEdgeEffectFactory(callback, context) + } + null + } else binding.swipeRefreshLayout + + initAdapter(binding.progressBar, swipeRefreshLayout, binding.list, binding.motionLayout, binding.errorLayout, adapter, headerAdapter ) return binding.root } + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return createView(inflater, container, savedInstanceState, false) + } fun onTabReClicked() { binding.list.limitedLengthSmoothScrollToPosition(0) } + + private fun onPullUp() { + // Handle the pull-up action + Log.e("bottom", "reached") + } + } diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt index bf8d47f6..f63f5456 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt @@ -32,7 +32,8 @@ import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao class FeedContentRepository @ExperimentalPagingApi constructor( private val db: AppDatabase, private val dao: FeedContentDao, - private val mediator: RemoteMediator + private val mediator: RemoteMediator, + private val conversationsId: String = "", ) { /** @@ -44,7 +45,7 @@ class FeedContentRepository @ExperimentalPagingApi const val user = db.userDao().getActiveUser()!! val pagingSourceFactory = { - dao.feedContent(user.user_id, user.instance_uri) + dao.feedContent(user.user_id, user.instance_uri, conversationsId) } return Pager( diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt index c4a2acbf..e8444de7 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt @@ -8,14 +8,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadState import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch import org.pixeldroid.app.databinding.FragmentFeedBinding import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.posts.feeds.launch diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt index 277fccb3..09f5d029 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt @@ -1,13 +1,11 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags import android.os.Bundle -import androidx.fragment.app.add import androidx.fragment.app.commit import androidx.fragment.app.replace import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityFollowersBinding import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment -import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG @@ -37,7 +35,7 @@ class HashTagActivity : BaseActivity() { supportFragmentManager.commit { setReorderingAllowed(true) - replace(R.id.followsFragment, args = arguments) + replace(R.id.conversationFragment, args = arguments) } } } diff --git a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt index a279ade2..9922dcc0 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt @@ -50,7 +50,7 @@ class FollowsActivity : BaseActivity() { supportFragmentManager.commit { setReorderingAllowed(true) - replace(R.id.followsFragment, args = arguments) + replace(R.id.conversationFragment, args = arguments) } } } diff --git a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt index 1de40d77..059152cb 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt @@ -32,14 +32,14 @@ class ArrangeTabsFragment: DialogFragment() { @Inject lateinit var db: AppDatabase - private val model: ArrangeTabsViewModel by viewModels { ArrangeTabsViewModelFactory(requireContext(), db) } + private val model: ArrangeTabsViewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val inflater: LayoutInflater = requireActivity().layoutInflater val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null) - model.initTabsChecked(dialogView) + model.initTabsChecked() val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs) val listAdapter = ListViewAdapter(model) diff --git a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt index 4e103a11..a4ce0f7c 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt @@ -1,9 +1,7 @@ package org.pixeldroid.app.settings -import android.content.Context -import android.view.View import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -11,24 +9,10 @@ import org.pixeldroid.app.utils.Tab import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import org.pixeldroid.app.utils.loadDbMenuTabs -import org.pixeldroid.app.utils.loadDefaultMenuTabs +import javax.inject.Inject - -class ArrangeTabsViewModelFactory( - private val context: Context, private val db: AppDatabase -) : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(ArrangeTabsViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return ArrangeTabsViewModel(context, db) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} - -class ArrangeTabsViewModel( - private val fragmentContext: Context, +@HiltViewModel +class ArrangeTabsViewModel @Inject constructor( private val db: AppDatabase ): ViewModel() { @@ -56,18 +40,17 @@ class ArrangeTabsViewModel( } } - fun initTabsChecked(view: View) { + fun initTabsChecked() { if (oldTabsChecked.isEmpty()) { // Only load tabsChecked if the model has not been updated _uiState.update { currentUiState -> currentUiState.copy( tabsChecked = if (_uiState.value.tabsDbEntities.isEmpty()) { // Load default menu - val list = loadDefaultMenuTabs(fragmentContext, view) - list.zip(List(list.size){true}.toTypedArray()).toList() + Tab.defaultTabs.zip(List(Tab.defaultTabs.size){true}) + Tab.otherTabs.zip(List(Tab.otherTabs.size){false}) } else { // Get current menu visibility and order from settings - loadDbMenuTabs(fragmentContext, _uiState.value.tabsDbEntities).toList() + loadDbMenuTabs(_uiState.value.tabsDbEntities).toList() } ) } diff --git a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt index ebcff40d..a4baff4f 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt @@ -18,7 +18,6 @@ import org.pixeldroid.app.main.MainActivity import org.pixeldroid.app.utils.setThemeFromPreferences import org.pixeldroid.common.ThemedActivity - @AndroidEntryPoint class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener { @@ -41,7 +40,11 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC // Handle the back button event // If a setting (for example language or theme) was changed, the main activity should be // started without history so that the change is applied to the whole back stack - if (restartMainOnExit) { + //TODO restore behaviour without true here, so that MainActivity is not destroyed when not necessary + // The true is a "temporary" (lol) fix so that tab changes are always taken into account + // Also, consider making the up button (arrow in action bar) also take this codepath! + // It recreates the activity by default + if (true || restartMainOnExit) { val intent = Intent(this@SettingsActivity, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK super@SettingsActivity.startActivity(intent) diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index 94891ab3..567dd309 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -12,14 +12,12 @@ import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.util.DisplayMetrics -import android.view.View import android.view.WindowManager import android.webkit.MimeTypeMap import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.PopupMenu import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver @@ -199,23 +197,14 @@ fun Fragment.bindingLifecycleAware(): ReadWriteProperty = } } -fun loadDefaultMenuTabs(context: Context, anchor: View): List { - return with(PopupMenu(context, anchor)) { - val menu = this.menu - menuInflater.inflate(R.menu.navigation_main, menu) - menu.removeGroup(R.id.bottomNavigationGroup) - (0 until menu.size()).map { Tab.fromLanguageString(context, menu.getItem(it).title.toString()) } - } -} - -fun loadDbMenuTabs(ctx: Context, tabsDbEntry: List): List> { +fun loadDbMenuTabs(tabsDbEntry: List): List> { return tabsDbEntry.map { Pair(Tab.fromName(it.tab), it.checked) } } enum class Tab { - HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED; + HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES; fun toLanguageString(ctx: Context): String { return ctx.getString( @@ -225,6 +214,7 @@ enum class Tab { CREATE_FEED -> R.string.create_feed NOTIFICATIONS_FEED -> R.string.notifications_feed PUBLIC_FEED -> R.string.public_feed + DIRECT_MESSAGES -> R.string.direct_messages } ) } @@ -240,6 +230,7 @@ enum class Tab { CREATE_FEED -> R.drawable.selector_camera NOTIFICATIONS_FEED -> R.drawable.selector_notifications PUBLIC_FEED -> R.drawable.ic_filter_black_24dp + DIRECT_MESSAGES -> R.drawable.selector_dm } return AppCompatResources.getDrawable(ctx, resId) } @@ -252,6 +243,7 @@ enum class Tab { ctx.getString(R.string.create_feed) -> CREATE_FEED ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED ctx.getString(R.string.public_feed) -> PUBLIC_FEED + ctx.getString(R.string.direct_messages) -> DIRECT_MESSAGES else -> HOME_FEED } } @@ -259,5 +251,18 @@ enum class Tab { fun fromName(name: String): Tab { return entries.filter { it.name == name }.getOrElse(0) { HOME_FEED } } + + val defaultTabs: List + get() = listOf( + HOME_FEED, + SEARCH_DISCOVER_FEED, + CREATE_FEED, + NOTIFICATIONS_FEED, + PUBLIC_FEED + ) + val otherTabs: List + get() = listOf( + DIRECT_MESSAGES + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index c67fb1bf..08e546ab 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -429,6 +429,30 @@ interface PixelfedAPI { @GET("/api/v1.1/discover/posts/hashtags") suspend fun trendingHashtags() : List + @GET("/api/v1/conversations") + suspend fun directMessagesList( +// @Query("max_id") max_id: String? = null, +// @Query("since_id") since_id: String? = null, + // @Query("min_id") min_id: String? = null, + @Query("page") page: Int? = null, + @Query("limit") limit: String? = null, + ): List + + @GET("/api/v1.1/direct/thread") + suspend fun directMessagesConversation( + @Query("pid") pid: String? = null, + @Query("max_id") max_id: String? = null, + @Query("min_id") min_id: String? = null, + ): DMThread + + @POST("/api/v1.1/direct/thread/send") + suspend fun sendDirectMessage( + @Query("to_id") to_id: String? = null, + @Query("message") message: String? = null, + // text or emoji + @Query("type") min_id: String = "text", + ): DMThread + @FormUrlEncoded @POST("/api/v1/reports") @JvmSuppressWildcards diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Conversation.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Conversation.kt new file mode 100644 index 00000000..370c5c45 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Conversation.kt @@ -0,0 +1,35 @@ +package org.pixeldroid.app.utils.api.objects + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity +import java.io.Serializable + +/* +Represents a conversation. +https://docs.joinmastodon.org/entities/Conversation/ + */ +@Entity( + tableName = "directMessages", + primaryKeys = ["id", "user_id", "instance_uri"], + foreignKeys = [ForeignKey( + entity = UserDatabaseEntity::class, + parentColumns = arrayOf("user_id", "instance_uri"), + childColumns = arrayOf("user_id", "instance_uri"), + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["user_id", "instance_uri"])] +) +data class Conversation( + override val id: String, + val unread: Boolean?, + val accounts: List?, + val last_status: Status?, + + //Database values (not from API) + //TODO do we find this approach acceptable? Preferable to a semi-duplicate ConversationDataBaseEntity? + override var user_id: String, + override var instance_uri: String, +): FeedContent, FeedContentDatabase, Serializable \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/DMThread.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/DMThread.kt new file mode 100644 index 00000000..2fe3fc42 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/DMThread.kt @@ -0,0 +1,29 @@ +package org.pixeldroid.app.utils.api.objects + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity +import java.io.Serializable +import java.time.Instant + + +/* +Represents a conversation. +https://docs.joinmastodon.org/entities/Conversation/ + */ +data class DMThread( + override val id: String, + val name: String?, + val username: String?, + val avatar: String?, + val url: String?, + val muted: Boolean?, + val isLocal: Boolean?, + val domain: String?, + val created_at: Instant?, //ISO 8601 Datetime + val updated_at: Instant?, + val timeAgo: String?, + val lastMessage: String?, + val messages: List, +): FeedContent, Serializable \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Message.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Message.kt new file mode 100644 index 00000000..059477c5 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Message.kt @@ -0,0 +1,19 @@ +package org.pixeldroid.app.utils.api.objects + +import java.io.Serializable +import java.time.Instant + +data class Message( + override val id: String, + val name: String?, + val hidden: Boolean?, + val isAuthor: Boolean?, + val type: String?, //TODO enum? + val text: String?, + val media: String?, //TODO, + val carousel: List?, + val created_at: Instant?, //ISO 8601 Datetime + val timeAgo: String?, + val reportId: String?, + //val meta: String?, //TODO +): FeedContent, Serializable diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt b/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt index 7c05d2ed..eab128d9 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/AppDatabase.kt @@ -6,15 +6,21 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import org.pixeldroid.app.utils.db.dao.* +import org.pixeldroid.app.utils.api.objects.Conversation +import org.pixeldroid.app.utils.api.objects.Notification +import org.pixeldroid.app.utils.db.dao.InstanceDao +import org.pixeldroid.app.utils.db.dao.TabsDao +import org.pixeldroid.app.utils.db.dao.UserDao +import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesConversationDao +import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesDao import org.pixeldroid.app.utils.db.dao.feedContent.NotificationDao import org.pixeldroid.app.utils.db.dao.feedContent.posts.HomePostDao import org.pixeldroid.app.utils.db.dao.feedContent.posts.PublicPostDao +import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity -import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity @Database(entities = [ @@ -23,13 +29,17 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity HomeStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class, Notification::class, - TabsDatabaseEntity::class + TabsDatabaseEntity::class, + Conversation::class, + DirectMessageDatabaseEntity::class, ], - version = 7, autoMigrations = [ - AutoMigration(from = 6, to = 7) + AutoMigration(from = 6, to = 7), + AutoMigration(from = 7, to = 8) ], + version = 8 ) + @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun instanceDao(): InstanceDao @@ -38,6 +48,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun publicPostDao(): PublicPostDao abstract fun notificationDao(): NotificationDao abstract fun tabsDao(): TabsDao + abstract fun directMessagesDao(): DirectMessagesDao + abstract fun directMessagesConversationDao(): DirectMessagesConversationDao } val MIGRATION_3_4 = object : Migration(3, 4) { @@ -56,4 +68,4 @@ val MIGRATION_5_6 = object : Migration(5, 6) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1") } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/Converters.kt b/app/src/main/java/org/pixeldroid/app/utils/db/Converters.kt index 71d9d32e..64aa3e19 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/Converters.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/Converters.kt @@ -137,6 +137,15 @@ class Converters { Status.Visibility::class.java ) + @TypeConverter + fun accountListToJson(type: List?): String { + val listType = object : TypeToken?>() {}.type + return gson.toJson(type, listType) + } - + @TypeConverter + fun jsonToAccountList(json: String): List? { + val listType = object : TypeToken?>() {}.type + return gson.fromJson(json, listType) + } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesConversationDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesConversationDao.kt new file mode 100644 index 00000000..4abeadc3 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesConversationDao.kt @@ -0,0 +1,25 @@ +package org.pixeldroid.app.utils.db.dao.feedContent + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import org.pixeldroid.app.utils.api.objects.Conversation +import org.pixeldroid.app.utils.api.objects.Notification +import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity + +@Dao +interface DirectMessagesConversationDao: FeedContentDao { + + @Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId") + override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) + + //TODO think about ordering + @Query("SELECT * FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) DESC") + override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource + + @Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND conversationsId=:conversationsId") + override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) + + @Query("SELECT id FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) ASC LIMIT 1") + suspend fun lastMessageId(userId: String, instanceUri: String, conversationsId: String): String? +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesDao.kt new file mode 100644 index 00000000..0c759a15 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/DirectMessagesDao.kt @@ -0,0 +1,20 @@ +package org.pixeldroid.app.utils.db.dao.feedContent + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import org.pixeldroid.app.utils.api.objects.Conversation + +@Dao +interface DirectMessagesDao: FeedContentDao { + + @Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''") + override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) + + //TODO think about ordering + @Query("""SELECT * FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""""") + override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource + + @Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''") + override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/FeedContentDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/FeedContentDao.kt index 31ef8e7d..42124812 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/FeedContentDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/FeedContentDao.kt @@ -7,13 +7,14 @@ import org.pixeldroid.app.utils.api.objects.FeedContentDatabase interface FeedContentDao{ - fun feedContent(userId: String, instanceUri: String): PagingSource + fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource - suspend fun clearFeedContent(userId: String, instanceUri: String) + suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) + suspend fun clearFeedContent(userId: String, instanceUri: String) = clearFeedContent(userId, instanceUri, "") @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(feedContent: List) - suspend fun delete(id: String, userId: String, instanceUri: String) - + suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) + suspend fun delete(id: String, userId: String, instanceUri: String) = delete(id, userId, instanceUri, "") } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt index 0c498190..6c0d7bba 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/NotificationDao.kt @@ -8,17 +8,17 @@ import org.pixeldroid.app.utils.api.objects.Notification @Dao interface NotificationDao: FeedContentDao { - @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri") - override suspend fun clearFeedContent(userId: String, instanceUri: String) + @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''") + override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) - @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri + @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId="" ORDER BY datetime(created_at) DESC""") - override fun feedContent(userId: String, instanceUri: String): PagingSource + override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri ORDER BY datetime(created_at) DESC LIMIT 1""") fun latestNotification(userId: String, instanceUri: String): Notification? - @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") - override suspend fun delete(id: String, userId: String, instanceUri: String) + @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''") + override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/HomePostDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/HomePostDao.kt index 07efde38..ad62e1b0 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/HomePostDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/HomePostDao.kt @@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity @Dao interface HomePostDao: FeedContentDao { - @Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri + @Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId="" ORDER BY datetime(created_at) DESC""") - override fun feedContent(userId: String, instanceUri: String): PagingSource + override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource - @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri") - override suspend fun clearFeedContent(userId: String, instanceUri: String) + @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''") + override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) - @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") - override suspend fun delete(id: String, userId: String, instanceUri: String) + @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''") + override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) @Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId") fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean) diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/PublicPostDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/PublicPostDao.kt index 918c96e2..a425233f 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/PublicPostDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/feedContent/posts/PublicPostDao.kt @@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity @Dao interface PublicPostDao: FeedContentDao { - @Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri + @Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId="" ORDER BY datetime(created_at) DESC""") - override fun feedContent(userId: String, instanceUri: String): PagingSource + override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource - @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri") - override suspend fun clearFeedContent(userId: String, instanceUri: String) + @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''") + override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String) - @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") - override suspend fun delete(id: String, userId: String, instanceUri: String) + @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''") + override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String) @Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId") fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean) diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/entities/DirectMessageDatabaseEntity.kt b/app/src/main/java/org/pixeldroid/app/utils/db/entities/DirectMessageDatabaseEntity.kt new file mode 100644 index 00000000..1dc38a42 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/db/entities/DirectMessageDatabaseEntity.kt @@ -0,0 +1,62 @@ +package org.pixeldroid.app.utils.db.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import org.pixeldroid.app.utils.api.objects.Attachment +import org.pixeldroid.app.utils.api.objects.FeedContent +import org.pixeldroid.app.utils.api.objects.FeedContentDatabase +import org.pixeldroid.app.utils.api.objects.Message +import java.io.Serializable +import java.time.Instant + +@Entity( + tableName = "directMessagesThreads", + primaryKeys = ["id", "conversationsId", "user_id", "instance_uri"], + foreignKeys = [ForeignKey( + entity = UserDatabaseEntity::class, + parentColumns = arrayOf("user_id", "instance_uri"), + childColumns = arrayOf("user_id", "instance_uri"), + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["user_id", "instance_uri", "conversationsId"])] +) +data class DirectMessageDatabaseEntity( + override val id: String, + val name: String?, + val hidden: Boolean?, + val isAuthor: Boolean?, + val type: String?, //TODO enum? + val text: String?, + val media: String?, //TODO, + val carousel: List?, + val created_at: Instant?, //ISO 8601 Datetime + val timeAgo: String?, + val reportId: String?, + //val meta: String?, //TODO + + // Database values (not from API) + val conversationsId: String, + override var user_id: String, + override var instance_uri: String, +): FeedContent, FeedContentDatabase, Serializable { + constructor(message: Message, conversationsId: String, user: UserDatabaseEntity) : this( + message.id, + message.name, + message.hidden, + message.isAuthor, + message.type, + message.text, + message.media, + message.carousel, + message.created_at, + message.timeAgo, + message.reportId, + //message.meta, + + conversationsId, + user.user_id, + user.instance_uri + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/message.xml b/app/src/main/res/drawable/message.xml new file mode 100644 index 00000000..5175b3f5 --- /dev/null +++ b/app/src/main/res/drawable/message.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/message_bubble_incoming.xml b/app/src/main/res/drawable/message_bubble_incoming.xml new file mode 100644 index 00000000..ddd6486f --- /dev/null +++ b/app/src/main/res/drawable/message_bubble_incoming.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_bubble_outgoing.xml b/app/src/main/res/drawable/message_bubble_outgoing.xml new file mode 100644 index 00000000..dcbbea2e --- /dev/null +++ b/app/src/main/res/drawable/message_bubble_outgoing.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_message.xml b/app/src/main/res/drawable/outline_message.xml new file mode 100644 index 00000000..20444443 --- /dev/null +++ b/app/src/main/res/drawable/outline_message.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/selector_dm.xml b/app/src/main/res/drawable/selector_dm.xml new file mode 100644 index 00000000..93f51e90 --- /dev/null +++ b/app/src/main/res/drawable/selector_dm.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 8affa9d7..6bc04a7e 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -35,7 +35,7 @@ android:id="@+id/tabs" android:layout_width="wrap_content" android:layout_height="match_parent" - app:menu="@menu/navigation_main" /> + tools:menu="@menu/navigation_main" /> + tools:menu="@menu/navigation_main" /> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation.xml b/app/src/main/res/layout/activity_conversation.xml new file mode 100644 index 00000000..c78bf936 --- /dev/null +++ b/app/src/main/res/layout/activity_conversation.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + +