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.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.cachedFeeds.notifications.NotificationsFragment import org.pixeldroid.app.posts.feeds.cachedFeeds.notifications.NotificationsFragment
import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment 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.profile.ProfileActivity
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.Tab import org.pixeldroid.app.utils.Tab
import org.pixeldroid.app.utils.api.objects.Notification 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.HomeStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@ -476,9 +478,17 @@ class MainActivity : BaseActivity() {
arguments = Bundle().apply { putBoolean("home", false) } arguments = Bundle().apply { putBoolean("home", false) }
} }
} } } }
Tab.DIRECT_MESSAGES -> {{ Tab.DIRECT_MESSAGES -> { {
DirectMessagesFragment() 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) if(tabs.contains(Tab.DIRECT_MESSAGES)) removeGroup(R.id.dmNavigationGroup)
} }
tabs.zip(pageIds).forEach { (tabId, pageId) -> val user = db.userDao().getActiveUser()!!
with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) {
val tabIcon = tabId.getDrawable(applicationContext) // 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) { if (tabIcon != null) {
this?.icon = tabIcon this?.icon = tabIcon
} }

View File

@ -4,11 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.StatusViewHolder import org.pixeldroid.app.posts.StatusViewHolder
import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR 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.api.objects.Tag.Companion.HASHTAG_TAG
import org.pixeldroid.app.utils.displayDimensionsInPx import org.pixeldroid.app.utils.displayDimensionsInPx
/** /**
* Fragment to show a list of [Status]es, as a result of a search or a hashtag. * 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) hashtagOrQuery = arguments?.getString(HASHTAG_TAG)
if(hashtagOrQuery == null){ if (hashtagOrQuery == null) {
search = true search = true
hashtagOrQuery = arguments?.getString("searchFeed")!! hashtagOrQuery = arguments?.getString("searchFeed")!!
} }

View File

@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -22,6 +23,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.Tab
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +41,8 @@ class ArrangeTabsFragment: DialogFragment() {
val inflater: LayoutInflater = requireActivity().layoutInflater val inflater: LayoutInflater = requireActivity().layoutInflater
val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null) 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 listFeed: RecyclerView = dialogView.findViewById(R.id.tabs)
val listAdapter = ListViewAdapter(model) val listAdapter = ListViewAdapter(model)
@ -71,7 +74,7 @@ class ArrangeTabsFragment: DialogFragment() {
// Save values into preferences // Save values into preferences
val tabsChecked = listAdapter.model.uiState.value.tabsChecked.toList() val tabsChecked = listAdapter.model.uiState.value.tabsChecked.toList()
val tabsDbEntity = tabsChecked.mapIndexed { index, (tab, checked) -> with (db.userDao().getActiveUser()!!) { 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 { lifecycleScope.launch {
db.tabsDao().clearAndRefill(tabsDbEntity, model.uiState.value.userId, model.uiState.value.instanceUri) 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 checkBox: MaterialCheckBox = holder.itemView.findViewById(R.id.checkBox)
val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle) val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle)
val tabText = model.getTabsButtons(position)
// Set content of each entry // 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 checkBox.isChecked = model.uiState.value.tabsChecked[position].second
// Also interact with checkbox when button is clicked fun callbackOnClickListener(tabNew: Tab, isCheckedNew: Boolean, hashtag: String? = null) {
textView.setOnClickListener { tabNew.filter = hashtag?.split("#")?.filter { it.isNotBlank() }?.get(0)
val isCheckedNew = !model.uiState.value.tabsChecked[position].second model.tabsCheckReplace(position, Pair(tabNew, isCheckedNew))
model.tabsCheckReplace(position, Pair(model.uiState.value.tabsChecked[position].first, isCheckedNew))
checkBox.isChecked = 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 // Disable OK button when no tab is selected or when strictly more than 5 tabs are selected
val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5 val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
with (model.uiState.value.tabsChecked.count { (_, v) -> v }) { this in 1..maxItemCount} 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 // Also highlight button when checkbox is clicked

View File

@ -1,6 +1,10 @@
package org.pixeldroid.app.settings package org.pixeldroid.app.settings
import android.content.Context
import android.widget.EditText
import androidx.lifecycle.ViewModel 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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -20,6 +24,7 @@ class ArrangeTabsViewModel @Inject constructor(
val uiState: StateFlow<ArrangeTabsUiState> = _uiState val uiState: StateFlow<ArrangeTabsUiState> = _uiState
private var oldTabsChecked: MutableList<Pair<Tab, Boolean>> = mutableListOf() private var oldTabsChecked: MutableList<Pair<Tab, Boolean>> = mutableListOf()
private var oldTabsButtons: MutableList<String?> = mutableListOf()
init { init {
initTabsDbEntities() initTabsDbEntities()
@ -40,7 +45,7 @@ class ArrangeTabsViewModel @Inject constructor(
} }
} }
fun initTabsChecked() { fun initTabsChecked(): Int {
if (oldTabsChecked.isEmpty()) { if (oldTabsChecked.isEmpty()) {
// Only load tabsChecked if the model has not been updated // Only load tabsChecked if the model has not been updated
_uiState.update { currentUiState -> _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>) { 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( data class ArrangeTabsUiState(
val userId: String = "", val userId: String = "",
val instanceUri: String = "", val instanceUri: String = "",
val tabsDbEntities: List<TabsDatabaseEntity> = listOf(), 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 com.google.gson.JsonSerializer
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
import java.time.Instant import java.time.Instant
import java.time.format.DateTimeFormatter 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>> { fun loadDbMenuTabs(tabsDbEntry: List<TabsDatabaseEntity>): List<Pair<Tab, Boolean>> {
return tabsDbEntry.map { 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 { enum class Tab(var filter: String? = null) {
HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES; HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES, HASHTAG_FEED;
fun toLanguageString(ctx: Context): String { fun toLanguageString(ctx: Context, db: AppDatabase, index: Int, lookForFilter: Boolean = false): String {
return ctx.getString( return when (this) {
when (this) { HOME_FEED -> ctx.getString(R.string.home_feed)
HOME_FEED -> R.string.home_feed SEARCH_DISCOVER_FEED -> ctx.getString(R.string.search_discover_feed)
SEARCH_DISCOVER_FEED -> R.string.search_discover_feed CREATE_FEED -> ctx.getString(R.string.create_feed)
CREATE_FEED -> R.string.create_feed NOTIFICATIONS_FEED -> ctx.getString(R.string.notifications_feed)
NOTIFICATIONS_FEED -> R.string.notifications_feed PUBLIC_FEED -> ctx.getString(R.string.public_feed)
PUBLIC_FEED -> R.string.public_feed DIRECT_MESSAGES -> ctx.getString(R.string.direct_messages)
DIRECT_MESSAGES -> 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 { fun toName(): String {
@ -231,6 +243,7 @@ enum class Tab {
NOTIFICATIONS_FEED -> R.drawable.selector_notifications NOTIFICATIONS_FEED -> R.drawable.selector_notifications
PUBLIC_FEED -> R.drawable.ic_filter_black_24dp PUBLIC_FEED -> R.drawable.ic_filter_black_24dp
DIRECT_MESSAGES -> R.drawable.selector_dm DIRECT_MESSAGES -> R.drawable.selector_dm
HASHTAG_FEED -> R.drawable.feed_hashtag
} }
return AppCompatResources.getDrawable(ctx, resId) return AppCompatResources.getDrawable(ctx, resId)
} }
@ -244,6 +257,7 @@ enum class Tab {
ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED
ctx.getString(R.string.public_feed) -> PUBLIC_FEED ctx.getString(R.string.public_feed) -> PUBLIC_FEED
ctx.getString(R.string.direct_messages) -> DIRECT_MESSAGES ctx.getString(R.string.direct_messages) -> DIRECT_MESSAGES
ctx.getString(R.string.feed_hashtag) -> HASHTAG_FEED
else -> HOME_FEED else -> HOME_FEED
} }
} }
@ -262,7 +276,8 @@ enum class Tab {
) )
val otherTabs: List<Tab> val otherTabs: List<Tab>
get() = listOf( get() = listOf(
DIRECT_MESSAGES DIRECT_MESSAGES,
HASHTAG_FEED
) )
} }
} }

View File

@ -35,9 +35,10 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
], ],
autoMigrations = [ autoMigrations = [
AutoMigration(from = 6, to = 7), 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) @TypeConverters(Converters::class)
@ -69,3 +70,5 @@ val MIGRATION_5_6 = object : Migration(5, 6) {
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1") 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 @Dao
interface TabsDao { interface TabsDao {
@Query("SELECT * FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri") @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") @Query("SELECT * FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri")
fun getTabsChecked(userId: String, instanceUri: String): List<TabsDatabaseEntity> fun getTabsChecked(userId: String, instanceUri: String): List<TabsDatabaseEntity>
@ -28,16 +28,6 @@ interface TabsDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTabChecked(tabChecked: TabsDatabaseEntity): Long 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 @Transaction
suspend fun clearAndRefill(tabsChecked: List<TabsDatabaseEntity>, userId: String, instanceUri: String) { suspend fun clearAndRefill(tabsChecked: List<TabsDatabaseEntity>, userId: String, instanceUri: String) {
deleteTabsChecked(userId, instanceUri) deleteTabsChecked(userId, instanceUri)

View File

@ -22,4 +22,5 @@ data class TabsDatabaseEntity(
var instance_uri: String, var instance_uri: String,
var tab: String, var tab: String,
var checked: Boolean = true, 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="dm_title">DM to %1$s</string>
<string name="direct_messages">Direct Messages</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> </resources>