Implement custom hashtag feed with ViewModel

This commit is contained in:
Fred 2024-08-28 19:22:44 +02:00
parent 5a10a2bae4
commit 4a2b266f01
11 changed files with 1155 additions and 43 deletions

View File

@ -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')"
]
}
}

View File

@ -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
}

View File

@ -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<Status>() {
hashtagOrQuery = arguments?.getString(HASHTAG_TAG)
if(hashtagOrQuery == null){
if (hashtagOrQuery == null) {
search = true
hashtagOrQuery = arguments?.getString("searchFeed")!!
}

View File

@ -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

View File

@ -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<ArrangeTabsUiState> = _uiState
private var oldTabsChecked: MutableList<Pair<Tab, Boolean>> = mutableListOf()
private var oldTabsButtons: MutableList<String?> = 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<Tab, Boolean>) {
@ -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<TabsDatabaseEntity> = listOf(),
val tabsChecked: List<Pair<Tab, Boolean>> = listOf()
val tabsChecked: List<Pair<Tab, Boolean>> = listOf(),
val tabsButtons: List<String?> = listOf()
)

View File

@ -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 <T> Fragment.bindingLifecycleAware(): ReadWriteProperty<Fragment, T> =
fun loadDbMenuTabs(tabsDbEntry: List<TabsDatabaseEntity>): List<Pair<Tab, Boolean>> {
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<Tab>
get() = listOf(
DIRECT_MESSAGES
DIRECT_MESSAGES,
HASHTAG_FEED
)
}
}

View File

@ -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")
}
}
}
// TODO: Manually add missing HASHTAG_FEED entry

View File

@ -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<TabsDatabaseEntity>
@ -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<TabsDatabaseEntity>, userId: String, instanceUri: String) {
deleteTabsChecked(userId, instanceUri)

View File

@ -22,4 +22,5 @@ data class TabsDatabaseEntity(
var instance_uri: String,
var tab: String,
var checked: Boolean = true,
var filter: String? = null
)

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,10L20,8h-4L16,4h-2v4h-4L10,4L8,4v4L4,8v2h4v4L4,14v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4h4zM14,14h-4v-4h4v4z"/>
</vector>

View File

@ -354,4 +354,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="dm_title">DM to %1$s</string>
<string name="direct_messages">Direct Messages</string>
<string name="feed_hashtag">Hashtag Feed</string>
<string name="feed_hashtag_description">Write a single hashtag below</string>
</resources>