diff --git a/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/9.json b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/9.json new file mode 100644 index 00000000..a8f4661f --- /dev/null +++ b/app/schemas/org.pixeldroid.app.utils.db.AppDatabase/9.json @@ -0,0 +1,991 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "5ee0e8fbaef28650cbea6670e24e08bb", + "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, `filter` TEXT, 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 + }, + { + "fieldPath": "filter", + "columnName": "filter", + "affinity": "TEXT", + "notNull": false + } + ], + "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, '5ee0e8fbaef28650cbea6670e24e08bb')" + ] + } +} \ 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 31592d64..0df32c93 100644 --- a/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt @@ -62,12 +62,14 @@ import org.pixeldroid.app.posts.NestedScrollableHost import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment import org.pixeldroid.app.posts.feeds.cachedFeeds.notifications.NotificationsFragment import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment +import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment import org.pixeldroid.app.profile.ProfileActivity import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment import org.pixeldroid.app.settings.SettingsActivity import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.Tab import org.pixeldroid.app.utils.api.objects.Notification +import org.pixeldroid.app.utils.api.objects.Tag import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity @@ -476,9 +478,17 @@ class MainActivity : BaseActivity() { arguments = Bundle().apply { putBoolean("home", false) } } } } - Tab.DIRECT_MESSAGES -> {{ + Tab.DIRECT_MESSAGES -> { { DirectMessagesFragment() - }} + } } + Tab.HASHTAG_FEED -> { { + UncachedPostsFragment() + .apply { + arguments = Bundle().apply { + putString(Tag.HASHTAG_TAG, this@getFragment.filter) + } + } + } } } } @@ -497,9 +507,25 @@ class MainActivity : BaseActivity() { if(tabs.contains(Tab.DIRECT_MESSAGES)) removeGroup(R.id.dmNavigationGroup) } - tabs.zip(pageIds).forEach { (tabId, pageId) -> - with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) { - val tabIcon = tabId.getDrawable(applicationContext) + val user = db.userDao().getActiveUser()!! + + // Get all hashtag feed indices + val hashtagIndices = db.tabsDao().getTabsChecked(user.user_id, user.instance_uri).filter { + it.checked + }.map { + if (Tab.fromName(it.tab) == Tab.HASHTAG_FEED) { + it.index + } else { + 0 + } + } + + hashtagIndices.zip(tabs).zip(pageIds).forEach { (indexPageId, pageId) -> + val index = indexPageId.first + val tabId = indexPageId.second + + with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(this@MainActivity, db, index, true))) { + val tabIcon = tabId.getDrawable(this@MainActivity) if (tabIcon != null) { this?.icon = tabIcon } diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt index 61c71cb5..9f553bc8 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt @@ -4,11 +4,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.paging.ExperimentalPagingApi import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.pixeldroid.app.R import org.pixeldroid.app.posts.StatusViewHolder import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR @@ -19,6 +23,7 @@ import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG import org.pixeldroid.app.utils.displayDimensionsInPx + /** * Fragment to show a list of [Status]es, as a result of a search or a hashtag. */ @@ -33,7 +38,7 @@ class UncachedPostsFragment : UncachedFeedFragment() { hashtagOrQuery = arguments?.getString(HASHTAG_TAG) - if(hashtagOrQuery == null){ + if (hashtagOrQuery == null) { search = true hashtagOrQuery = arguments?.getString("searchFeed")!! } 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 059152cb..0d268c10 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText import android.widget.FrameLayout import android.widget.ImageView import androidx.appcompat.app.AlertDialog @@ -22,6 +23,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.pixeldroid.app.R +import org.pixeldroid.app.utils.Tab import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import javax.inject.Inject @@ -39,7 +41,8 @@ class ArrangeTabsFragment: DialogFragment() { val inflater: LayoutInflater = requireActivity().layoutInflater val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null) - model.initTabsChecked() + val itemCount = model.initTabsChecked() + model.initTabsButtons(itemCount, requireContext()) val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs) val listAdapter = ListViewAdapter(model) @@ -71,7 +74,7 @@ class ArrangeTabsFragment: DialogFragment() { // Save values into preferences val tabsChecked = listAdapter.model.uiState.value.tabsChecked.toList() val tabsDbEntity = tabsChecked.mapIndexed { index, (tab, checked) -> with (db.userDao().getActiveUser()!!) { - TabsDatabaseEntity(index, user_id, instance_uri, tab.name, checked) + TabsDatabaseEntity(index, user_id, instance_uri, tab.name, checked, tab.filter) } } lifecycleScope.launch { db.tabsDao().clearAndRefill(tabsDbEntity, model.uiState.value.userId, model.uiState.value.instanceUri) @@ -103,20 +106,59 @@ class ArrangeTabsFragment: DialogFragment() { val checkBox: MaterialCheckBox = holder.itemView.findViewById(R.id.checkBox) val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle) + val tabText = model.getTabsButtons(position) + // Set content of each entry - textView.text = model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext()) + if (tabText != null) { + textView.text = tabText + } else { + model.updateTabsButtons(position, model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext(), db, position, true)) + textView.text = model.getTabsButtons(position) + } checkBox.isChecked = model.uiState.value.tabsChecked[position].second - // Also interact with checkbox when button is clicked - textView.setOnClickListener { - val isCheckedNew = !model.uiState.value.tabsChecked[position].second - model.tabsCheckReplace(position, Pair(model.uiState.value.tabsChecked[position].first, isCheckedNew)) + fun callbackOnClickListener(tabNew: Tab, isCheckedNew: Boolean, hashtag: String? = null) { + tabNew.filter = hashtag?.split("#")?.filter { it.isNotBlank() }?.get(0) + model.tabsCheckReplace(position, Pair(tabNew, isCheckedNew)) checkBox.isChecked = isCheckedNew + val hashtagWithHashtag = tabNew.filter?.let { + StringBuilder("#").append(it).toString() + } + // Disable OK button when no tab is selected or when strictly more than 5 tabs are selected val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5 (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = with (model.uiState.value.tabsChecked.count { (_, v) -> v }) { this in 1..maxItemCount} + model.updateTabsButtons(position, hashtagWithHashtag ?: model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext(), db, position, false)) + textView.text = model.getTabsButtons(position) + } + + // Also interact with checkbox when button is clicked + textView.setOnClickListener { + val isCheckedNew = !model.uiState.value.tabsChecked[position].second + val tabNew = model.uiState.value.tabsChecked[position].first + + if (tabNew == Tab.HASHTAG_FEED && isCheckedNew) { + // Ask which hashtag should filter + val textField = EditText(requireContext()) + + // Set the default text to a link of the Queen + textField.hint = "hashtag" + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.feed_hashtag)) + .setMessage(getString(R.string.feed_hashtag_description)) + .setView(textField) + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setPositiveButton(android.R.string.ok) { _, _ -> + val hashtag = textField.text.toString() + callbackOnClickListener(tabNew, isCheckedNew, hashtag) + } + .show() + } else { + callbackOnClickListener(tabNew, isCheckedNew) + } } // Also highlight button when checkbox is clicked 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 a4ce0f7c..9d0347fb 100644 --- a/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/settings/ArrangeTabsViewModel.kt @@ -1,6 +1,10 @@ package org.pixeldroid.app.settings +import android.content.Context +import android.widget.EditText import androidx.lifecycle.ViewModel +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,6 +24,7 @@ class ArrangeTabsViewModel @Inject constructor( val uiState: StateFlow = _uiState private var oldTabsChecked: MutableList> = mutableListOf() + private var oldTabsButtons: MutableList = mutableListOf() init { initTabsDbEntities() @@ -40,7 +45,7 @@ class ArrangeTabsViewModel @Inject constructor( } } - fun initTabsChecked() { + fun initTabsChecked(): Int { if (oldTabsChecked.isEmpty()) { // Only load tabsChecked if the model has not been updated _uiState.update { currentUiState -> @@ -55,6 +60,18 @@ class ArrangeTabsViewModel @Inject constructor( ) } } + return _uiState.value.tabsChecked.size + } + + fun initTabsButtons(itemCount: Int, ctx: Context) { + oldTabsChecked = _uiState.value.tabsChecked.toMutableList() + if (oldTabsButtons.isEmpty()) { + _uiState.update { currentUiState -> + currentUiState.copy( + tabsButtons = (0 until itemCount).map { null } + ) + } + } } fun tabsCheckReplace(position: Int, pair: Pair) { @@ -87,11 +104,26 @@ class ArrangeTabsViewModel @Inject constructor( ) } } + + fun updateTabsButtons(position: Int, text: String) { + oldTabsButtons = _uiState.value.tabsButtons.toMutableList() + oldTabsButtons[position] = text + _uiState.update { currentUiState -> + currentUiState.copy( + tabsButtons = oldTabsButtons.toList() + ) + } + } + + fun getTabsButtons(position: Int): String? { + return _uiState.value.tabsButtons[position] + } } data class ArrangeTabsUiState( val userId: String = "", val instanceUri: String = "", val tabsDbEntities: List = listOf(), - val tabsChecked: List> = listOf() + val tabsChecked: List> = listOf(), + val tabsButtons: List = listOf() ) \ No newline at end of file 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 567dd309..29eea4df 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -31,6 +31,7 @@ import com.google.gson.JsonPrimitive import com.google.gson.JsonSerializer import okhttp3.HttpUrl import org.pixeldroid.app.R +import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import java.time.Instant import java.time.format.DateTimeFormatter @@ -199,24 +200,35 @@ fun Fragment.bindingLifecycleAware(): ReadWriteProperty = fun loadDbMenuTabs(tabsDbEntry: List): List> { return tabsDbEntry.map { - Pair(Tab.fromName(it.tab), it.checked) + val tab = Tab.fromName(it.tab) + if (tab == Tab.HASHTAG_FEED) { + tab.filter = it.filter + } + Pair(tab, it.checked) } } -enum class Tab { - HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES; +enum class Tab(var filter: String? = null) { + HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES, HASHTAG_FEED; - fun toLanguageString(ctx: Context): String { - return ctx.getString( - when (this) { - HOME_FEED -> R.string.home_feed - SEARCH_DISCOVER_FEED -> R.string.search_discover_feed - CREATE_FEED -> R.string.create_feed - NOTIFICATIONS_FEED -> R.string.notifications_feed - PUBLIC_FEED -> R.string.public_feed - DIRECT_MESSAGES -> R.string.direct_messages + fun toLanguageString(ctx: Context, db: AppDatabase, index: Int, lookForFilter: Boolean = false): String { + return when (this) { + HOME_FEED -> ctx.getString(R.string.home_feed) + SEARCH_DISCOVER_FEED -> ctx.getString(R.string.search_discover_feed) + CREATE_FEED -> ctx.getString(R.string.create_feed) + NOTIFICATIONS_FEED -> ctx.getString(R.string.notifications_feed) + PUBLIC_FEED -> ctx.getString(R.string.public_feed) + DIRECT_MESSAGES -> ctx.getString(R.string.direct_messages) + HASHTAG_FEED -> { + val user = db.userDao().getActiveUser()!! + val hashtag = db.tabsDao().getTabChecked(index, user.user_id, user.instance_uri)?.filter + if (lookForFilter && hashtag != null) { + StringBuilder("#").append(hashtag).toString() + } else { + ctx.getString(R.string.feed_hashtag) + } } - ) + } } fun toName(): String { @@ -231,6 +243,7 @@ enum class Tab { NOTIFICATIONS_FEED -> R.drawable.selector_notifications PUBLIC_FEED -> R.drawable.ic_filter_black_24dp DIRECT_MESSAGES -> R.drawable.selector_dm + HASHTAG_FEED -> R.drawable.feed_hashtag } return AppCompatResources.getDrawable(ctx, resId) } @@ -244,6 +257,7 @@ enum class Tab { ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED ctx.getString(R.string.public_feed) -> PUBLIC_FEED ctx.getString(R.string.direct_messages) -> DIRECT_MESSAGES + ctx.getString(R.string.feed_hashtag) -> HASHTAG_FEED else -> HOME_FEED } } @@ -262,7 +276,8 @@ enum class Tab { ) val otherTabs: List get() = listOf( - DIRECT_MESSAGES + DIRECT_MESSAGES, + HASHTAG_FEED ) } } \ No newline at end of file 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 eab128d9..e5d6247b 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 @@ -35,9 +35,10 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity ], autoMigrations = [ AutoMigration(from = 6, to = 7), - AutoMigration(from = 7, to = 8) + AutoMigration(from = 7, to = 8), + AutoMigration(from = 8, to = 9) ], - version = 8 + version = 9 ) @TypeConverters(Converters::class) @@ -68,4 +69,6 @@ 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 +} + +// TODO: Manually add missing HASHTAG_FEED entry \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt index 3e899aa8..32c9a2bb 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/TabsDao.kt @@ -11,7 +11,7 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity @Dao interface TabsDao { @Query("SELECT * FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri") - fun getTabChecked(index: Int, userId: String, instanceUri: String): TabsDatabaseEntity + fun getTabChecked(index: Int, userId: String, instanceUri: String): TabsDatabaseEntity? @Query("SELECT * FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri") fun getTabsChecked(userId: String, instanceUri: String): List @@ -28,16 +28,6 @@ interface TabsDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertTabChecked(tabChecked: TabsDatabaseEntity): Long - @Update - suspend fun updateTabChecked(tabChecked: TabsDatabaseEntity) - - @Transaction - suspend fun insertOrUpdate(tabChecked: TabsDatabaseEntity) { - if (insertTabChecked(tabChecked) == -1L) { - updateTabChecked(tabChecked) - } - } - @Transaction suspend fun clearAndRefill(tabsChecked: List, userId: String, instanceUri: String) { deleteTabsChecked(userId, instanceUri) diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt b/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt index afc53a67..2e213c93 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/entities/TabsDatabaseEntity.kt @@ -22,4 +22,5 @@ data class TabsDatabaseEntity( var instance_uri: String, var tab: String, var checked: Boolean = true, + var filter: String? = null ) \ No newline at end of file diff --git a/app/src/main/res/drawable/feed_hashtag.xml b/app/src/main/res/drawable/feed_hashtag.xml new file mode 100644 index 00000000..e18136c6 --- /dev/null +++ b/app/src/main/res/drawable/feed_hashtag.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12d2c243..6974e711 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,4 +354,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" DM to %1$s Direct Messages + Hashtag Feed + Write a single hashtag below