Merge branch 'view_model_fixes' into 'master'

Direct Messages

Closes #389

See merge request pixeldroid/PixelDroid!596
This commit is contained in:
Matthieu 2024-08-24 15:12:21 +00:00
commit 100ae532c3
52 changed files with 2723 additions and 182 deletions

View File

@ -56,6 +56,10 @@ android {
testInstrumentationRunner "org.pixeldroid.app.testUtility.TestRunner" testInstrumentationRunner "org.pixeldroid.app.testUtility.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/java' main.java.srcDirs += 'src/main/java'

View File

@ -0,0 +1,985 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "37b7f1d842d148e1d117ac9caae8fb51",
"entities": [
{
"tableName": "instances",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
"fields": [
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "maxStatusChars",
"columnName": "maxStatusChars",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxPhotoSize",
"columnName": "maxPhotoSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxVideoSize",
"columnName": "maxVideoSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "albumLimit",
"columnName": "albumLimit",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "videoEnabled",
"columnName": "videoEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pixelfed",
"columnName": "pixelfed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uri"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "display_name",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar_static",
"columnName": "avatar_static",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "refreshToken",
"columnName": "refreshToken",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "clientId",
"columnName": "clientId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "clientSecret",
"columnName": "clientSecret",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_users_instance_uri",
"unique": false,
"columnNames": [
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
}
],
"foreignKeys": [
{
"table": "instances",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"instance_uri"
],
"referencedColumns": [
"uri"
]
}
]
},
{
"tableName": "homePosts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created_at",
"columnName": "created_at",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "spoiler_text",
"columnName": "spoiler_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "media_attachments",
"columnName": "media_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogs_count",
"columnName": "reblogs_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "favourites_count",
"columnName": "favourites_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replies_count",
"columnName": "replies_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "in_reply_to_id",
"columnName": "in_reply_to_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "in_reply_to_account",
"columnName": "in_reply_to_account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblog",
"columnName": "reblog",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_homePosts_user_id_instance_uri",
"unique": false,
"columnNames": [
"user_id",
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
},
{
"tableName": "publicPosts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created_at",
"columnName": "created_at",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "spoiler_text",
"columnName": "spoiler_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "media_attachments",
"columnName": "media_attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogs_count",
"columnName": "reblogs_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "favourites_count",
"columnName": "favourites_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replies_count",
"columnName": "replies_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "in_reply_to_id",
"columnName": "in_reply_to_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "in_reply_to_account",
"columnName": "in_reply_to_account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblog",
"columnName": "reblog",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_publicPosts_user_id_instance_uri",
"unique": false,
"columnNames": [
"user_id",
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
},
{
"tableName": "notifications",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created_at",
"columnName": "created_at",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_notifications_user_id_instance_uri",
"unique": false,
"columnNames": [
"user_id",
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
},
{
"tableName": "tabsChecked",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tab",
"columnName": "tab",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "checked",
"columnName": "checked",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"index",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_tabsChecked_user_id_instance_uri",
"unique": false,
"columnNames": [
"user_id",
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
},
{
"tableName": "directMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `unread` INTEGER, `accounts` TEXT, `last_status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "accounts",
"columnName": "accounts",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "last_status",
"columnName": "last_status",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_directMessages_user_id_instance_uri",
"unique": false,
"columnNames": [
"user_id",
"instance_uri"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessages_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
},
{
"tableName": "directMessagesThreads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `hidden` INTEGER, `isAuthor` INTEGER, `type` TEXT, `text` TEXT, `media` TEXT, `carousel` TEXT, `created_at` TEXT, `timeAgo` TEXT, `reportId` TEXT, `conversationsId` TEXT NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `conversationsId`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hidden",
"columnName": "hidden",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isAuthor",
"columnName": "isAuthor",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "media",
"columnName": "media",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "carousel",
"columnName": "carousel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created_at",
"columnName": "created_at",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timeAgo",
"columnName": "timeAgo",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reportId",
"columnName": "reportId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "conversationsId",
"columnName": "conversationsId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "user_id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "instance_uri",
"columnName": "instance_uri",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"conversationsId",
"user_id",
"instance_uri"
]
},
"indices": [
{
"name": "index_directMessagesThreads_user_id_instance_uri_conversationsId",
"unique": false,
"columnNames": [
"user_id",
"instance_uri",
"conversationsId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessagesThreads_user_id_instance_uri_conversationsId` ON `${TABLE_NAME}` (`user_id`, `instance_uri`, `conversationsId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"user_id",
"instance_uri"
],
"referencedColumns": [
"user_id",
"instance_uri"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '37b7f1d842d148e1d117ac9caae8fb51')"
]
}
}

View File

@ -141,6 +141,12 @@
<activity android:name=".searchDiscover.TrendingActivity" <activity android:name=".searchDiscover.TrendingActivity"
android:theme="@style/BaseAppTheme" /> android:theme="@style/BaseAppTheme" />
<activity android:name=".directmessages.ConversationActivity"
android:theme="@style/BaseAppTheme"
android:windowSoftInputMode="adjustPan" />
<activity android:name="org.pixeldroid.app.directmessages.DirectMessagesActivity"
android:theme="@style/BaseAppTheme" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"

View File

@ -0,0 +1,95 @@
package org.pixeldroid.app.directmessages
import android.content.Context
import android.graphics.Canvas
import android.widget.EdgeEffect
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import androidx.recyclerview.widget.RecyclerView
import org.pixeldroid.common.pxToDp
import kotlin.math.absoluteValue
/** The magnitude of translation distance while the list is over-scrolled. */
private const val OVERSCROLL_TRANSLATION_MAGNITUDE = 0.4f
/** The magnitude of translation distance when the list reaches the edge on fling. */
private const val FLING_TRANSLATION_MAGNITUDE = 0.5f
/**
* Replace edge effect by a bounce
*/
class BounceEdgeEffectFactory(val refreshCallback: () -> Unit, val context: Context) : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {
return object : EdgeEffect(recyclerView.context) {
// A reference to the [SpringAnimation] for this RecyclerView used to bring the item back after the over-scroll effect.
var translationAnim: SpringAnimation? = null
override fun onPull(deltaDistance: Float) {
super.onPull(deltaDistance)
handlePull(deltaDistance)
}
override fun onPull(deltaDistance: Float, displacement: Float) {
super.onPull(deltaDistance, displacement)
handlePull(deltaDistance)
}
private fun handlePull(deltaDistance: Float) {
// This is called on every touch event while the list is scrolled with a finger.
// Translate the recyclerView with the distance
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationYDelta = sign * recyclerView.width * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE
recyclerView.translationY += translationYDelta
translationAnim?.cancel()
}
override fun onRelease() {
super.onRelease()
// The finger is lifted. Start the animation to bring translation back to the resting state.
if (recyclerView.translationY != 0f) {
if (direction == DIRECTION_BOTTOM && recyclerView.translationY.toInt().absoluteValue.pxToDp(context) > 50) {
refreshCallback()
}
translationAnim = createAnim()?.also { it.start() }
}
}
override fun onAbsorb(velocity: Int) {
super.onAbsorb(velocity)
// The list has reached the edge on fling.
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
translationAnim?.cancel()
translationAnim = createAnim().setStartVelocity(translationVelocity)?.also { it.start() }
}
override fun draw(canvas: Canvas?): Boolean {
// don't paint the usual edge effect
return false
}
override fun isFinished(): Boolean {
// Without this, will skip future calls to onAbsorb()
return translationAnim?.isRunning?.not() ?: true
}
private fun createAnim() = SpringAnimation(recyclerView, SpringAnimation.TRANSLATION_Y)
.setSpring(
SpringForce()
.setFinalPosition(0f)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
.setStiffness(SpringForce.STIFFNESS_LOW)
)
}
}
}

View File

@ -0,0 +1,109 @@
package org.pixeldroid.app.directmessages
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityConversationBinding
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
class ConversationActivity : BaseActivity() {
lateinit var binding: ActivityConversationBinding
private lateinit var conversationFragment: ConversationFragment
companion object {
const val USERNAME = "ConversationActivityUsername"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityConversationBinding.inflate(layoutInflater)
conversationFragment = ConversationFragment()
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val userName = intent?.getSerializableExtra(USERNAME) as? String?
supportActionBar?.title = getString(R.string.dm_title, userName)
val conversationId = intent?.getSerializableExtra(CONVERSATION_ID) as String
val pid = intent?.getSerializableExtra(PROFILE_ID) as String
activateCommenter(pid)
initConversationFragment(pid, conversationId, savedInstanceState)
}
private fun activateCommenter(pid: String) {
//Activate commenter
binding.submitComment.setOnClickListener {
val textIn = binding.editComment.text
//Open text input
if(textIn.isNullOrEmpty()) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.empty_comment),
Toast.LENGTH_SHORT
).show()
} else {
//Post the comment
lifecycleScope.launchWhenCreated {
apiHolder.api?.let { it1 -> sendMessage(it1, pid) }
}
}
}
}
private fun initConversationFragment(profileId: String, conversationId: String, savedInstanceState: Bundle?) {
val arguments = Bundle()
arguments.putSerializable(CONVERSATION_ID, conversationId)
arguments.putSerializable(PROFILE_ID, profileId)
conversationFragment.arguments = arguments
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.conversationFragment, conversationFragment)
}
}
}
private suspend fun sendMessage(
api: PixelfedAPI,
pid: String,
) {
val textIn = binding.editComment.text
val nonNullText = textIn.toString()
try {
binding.submitComment.isEnabled = false
binding.editComment.isEnabled = false
api.sendDirectMessage(pid, nonNullText)
//Reload to add the comment to the comment section
conversationFragment.adapter.refresh()
binding.editComment.isEnabled = true
binding.editComment.text = null
binding.submitComment.isEnabled = true
} catch (exception: Exception) {
Log.e("DM SEND ERROR", exception.toString())
Toast.makeText(
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
}
}
}

View File

@ -0,0 +1,188 @@
package org.pixeldroid.app.directmessages
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.DirectMessagesConversationItemBinding
import org.pixeldroid.app.posts.AlbumActivity
import org.pixeldroid.app.posts.AlbumViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedContentRepository
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
import org.pixeldroid.app.utils.api.objects.Conversation
import org.pixeldroid.app.utils.api.objects.Message
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
/**
* Fragment for one Direct Messages conversation
*/
class ConversationFragment : CachedFeedFragment<DirectMessageDatabaseEntity>() {
companion object {
const val CONVERSATION_ID = "ConversationFragmentConversationId"
const val PROFILE_ID = "ConversationFragmentProfileId"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = DirectMessagesListAdapter(apiHolder)
}
@OptIn(ExperimentalPagingApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.createView(inflater, container, savedInstanceState, true)
val pid = arguments?.getSerializable(PROFILE_ID) as String
val conversationId = arguments?.getSerializable(CONVERSATION_ID) as String
val dao = db.directMessagesConversationDao()
val remoteMediator = ConversationRemoteMediator(apiHolder, db, pid, conversationId)
// get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(
requireActivity(),
ViewModelFactory(
db, dao, remoteMediator,
FeedContentRepository(db, dao, remoteMediator, conversationId)
)
)["directMessagesConversation", FeedViewModel::class.java] as FeedViewModel<DirectMessageDatabaseEntity>
launch()
initSearch()
return view
}
/**
* View Holder for a [Conversation] RecyclerView list item.
*/
class DirectMessagesConversationViewHolder(val binding: DirectMessagesConversationItemBinding) : RecyclerView.ViewHolder(binding.root) {
private var message: DirectMessageDatabaseEntity? = null
init {
itemView.setOnClickListener {
message?.let {
if (it.type == "photo") {
val intent = Intent(itemView.context, AlbumActivity::class.java)
intent.putExtra(AlbumViewModel.ALBUM_IMAGES, ArrayList(it.carousel.orEmpty()))
intent.putExtra(AlbumViewModel.ALBUM_INDEX, 0)
itemView.context.startActivity(intent)
}
}
}
}
fun bind(
message: DirectMessageDatabaseEntity?,
api: PixelfedAPIHolder,
lifecycleScope: LifecycleCoroutineScope,
) {
this.message = message
if(message?.isAuthor == true) {
binding.messageIncoming.visibility = GONE
binding.messageOutgoing.visibility = VISIBLE
binding.textMessageOutgoing.text = message.text
} else {
binding.messageIncoming.visibility = VISIBLE
binding.messageOutgoing.visibility = GONE
binding.textMessageIncoming.text = message?.text ?: ""
}
if (message?.type == "photo"){
binding.imageMessageIncoming.visibility = VISIBLE
binding.imageMessageOutgoing.visibility = VISIBLE
binding.textMessageOutgoing.visibility = GONE
binding.textMessageOutgoing.visibility = GONE
Glide.with(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming)
.load(message.media)
.into(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming)
} else {
binding.imageMessageIncoming.visibility = GONE
binding.imageMessageOutgoing.visibility = GONE
binding.textMessageOutgoing.visibility = VISIBLE
binding.textMessageIncoming.visibility = VISIBLE
}
message?.created_at.let {
// if (it == null) binding.messageTime.text = ""
// else setTextViewFromISO8601(
// it,
// binding.messageTime,
// false
// )
}
}
companion object {
fun create(parent: ViewGroup): DirectMessagesConversationViewHolder {
val itemBinding = DirectMessagesConversationItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return DirectMessagesConversationViewHolder(itemBinding)
}
}
}
inner class DirectMessagesListAdapter(
private val apiHolder: PixelfedAPIHolder,
) : PagingDataAdapter<DirectMessageDatabaseEntity, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<DirectMessageDatabaseEntity>() {
override fun areItemsTheSame(
oldItem: DirectMessageDatabaseEntity,
newItem: DirectMessageDatabaseEntity
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: DirectMessageDatabaseEntity,
newItem: DirectMessageDatabaseEntity
): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DirectMessagesConversationViewHolder.create(parent)
}
override fun getItemViewType(position: Int): Int {
return R.layout.direct_messages_conversation_item
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel?.let {
(holder as DirectMessagesConversationViewHolder).bind(
it,
apiHolder,
lifecycleScope
)
}
}
}
}

View File

@ -0,0 +1,74 @@
package org.pixeldroid.app.directmessages
import android.util.Log
import androidx.paging.*
import androidx.room.withTransaction
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
import java.lang.Exception
import java.lang.NullPointerException
import javax.inject.Inject
/**
* RemoteMediator for a Direct Messages conversation.
*
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
* a local db cache.
*/
@OptIn(ExperimentalPagingApi::class)
class ConversationRemoteMediator @Inject constructor(
private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase,
private val pid: String,
private val conversationId: String
) : RemoteMediator<Int, DirectMessageDatabaseEntity>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessageDatabaseEntity>): MediatorResult {
try {
val user = db.userDao().getActiveUser()
?: return MediatorResult.Error(NullPointerException("No active user exists"))
val nextPage = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> {
// No prepend for the moment, might be nice to add later
db.directMessagesConversationDao().lastMessageId(user.user_id, user.instance_uri, conversationId)
?: return MediatorResult.Success(endOfPaginationReached = true)
}
LoadType.APPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
}
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val apiResponse =
api.directMessagesConversation(
pid = pid,
max_id = nextPage,
)
//TODO prepend
val messages = apiResponse.messages.map {
DirectMessageDatabaseEntity(
it,
conversationId,
user
)
}
val endOfPaginationReached = messages.isEmpty()
db.withTransaction {
// Clear table in the database
if (loadType == LoadType.REFRESH) {
db.directMessagesConversationDao().clearFeedContent(user.user_id, user.instance_uri, conversationId)
}
db.directMessagesConversationDao().insertAll(messages)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: Exception){
return MediatorResult.Error(exception)
}
}
}

View File

@ -0,0 +1,40 @@
package org.pixeldroid.app.directmessages
import android.os.Bundle
import androidx.fragment.app.commit
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityConversationBinding
import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.profile.FollowsActivity
import org.pixeldroid.app.utils.BaseActivity
class DirectMessagesActivity : BaseActivity() {
lateinit var binding: ActivityFollowersBinding
private lateinit var conversationFragment: DirectMessagesFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFollowersBinding.inflate(layoutInflater)
conversationFragment = DirectMessagesFragment()
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.direct_messages)
initConversationFragment(savedInstanceState)
}
private fun initConversationFragment(savedInstanceState: Bundle?) {
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.conversationFragment, conversationFragment)
}
}
}
}

View File

@ -0,0 +1,171 @@
package org.pixeldroid.app.directmessages
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.DirectMessagesListItemBinding
import org.pixeldroid.app.directmessages.ConversationActivity.Companion.USERNAME
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
import org.pixeldroid.app.posts.parseHTMLText
import org.pixeldroid.app.posts.setTextViewFromISO8601
import org.pixeldroid.app.profile.ProfileActivity
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Conversation
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
/**
* Fragment for the list of Direct Messages conversations.
*/
class DirectMessagesFragment : CachedFeedFragment<Conversation>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = DirectMessagesListAdapter(apiHolder)
}
@OptIn(ExperimentalPagingApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
// get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(
requireActivity(),
ViewModelFactory(db, db.directMessagesDao(), DirectMessagesRemoteMediator(apiHolder, db))
)["directMessagesList", FeedViewModel::class.java] as FeedViewModel<Conversation>
launch()
initSearch()
return view
}
/**
* View Holder for a [Conversation] RecyclerView list item.
*/
class DirectMessagesListViewHolder(val binding: DirectMessagesListItemBinding) : RecyclerView.ViewHolder(binding.root) {
private var conversation: Conversation? = null
init {
itemView.setOnClickListener {
conversation?.accounts?.firstOrNull()?.let {
val intent = Intent(itemView.context, ConversationActivity::class.java).apply {
putExtra(PROFILE_ID, it.id)
putExtra(CONVERSATION_ID, conversation?.id)
putExtra(USERNAME, it.getDisplayName())
}
itemView.context.startActivity(intent)
}
}
binding.dmAvatar.setOnClickListener {
conversation?.accounts?.firstOrNull()?.let {
val intent = Intent(itemView.context, ProfileActivity::class.java).apply {
putExtra(Account.ACCOUNT_TAG, it)
}
itemView.context.startActivity(intent)
}
}
}
fun bind(
conversation: Conversation?,
api: PixelfedAPIHolder,
lifecycleScope: LifecycleCoroutineScope,
) {
this.conversation = conversation
val account = conversation?.accounts?.firstOrNull()
Glide.with(itemView).load(account?.anyAvatar()).circleCrop()
.into(binding.dmAvatar)
binding.dmUsername.text = account?.getDisplayName()
binding.dmLastMessage.text = parseHTMLText(
conversation?.last_status?.content ?: "",
conversation?.last_status?.mentions,
api,
itemView.context,
lifecycleScope
)
conversation?.last_status?.created_at.let {
if (it == null) binding.messageTime.text = ""
else setTextViewFromISO8601(
it,
binding.messageTime,
false
)
}
}
companion object {
fun create(parent: ViewGroup): DirectMessagesListViewHolder {
val itemBinding = DirectMessagesListItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return DirectMessagesListViewHolder(itemBinding)
}
}
}
inner class DirectMessagesListAdapter(
private val apiHolder: PixelfedAPIHolder,
) : PagingDataAdapter<Conversation, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Conversation>() {
override fun areItemsTheSame(
oldItem: Conversation,
newItem: Conversation
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: Conversation,
newItem: Conversation
): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DirectMessagesListViewHolder.create(parent)
}
override fun getItemViewType(position: Int): Int {
return R.layout.direct_messages_list_item
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel?.let {
(holder as DirectMessagesListViewHolder).bind(
it,
apiHolder,
lifecycleScope
)
}
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pixeldroid.app.directmessages
import androidx.paging.*
import androidx.paging.PagingSource.LoadResult
import androidx.room.withTransaction
import org.pixeldroid.app.utils.api.objects.Conversation
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.api.objects.Notification
import retrofit2.HttpException
import java.lang.Exception
import java.lang.NullPointerException
import javax.inject.Inject
/**
* RemoteMediator for the notifications.
*
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
* a local db cache.
*/
@OptIn(ExperimentalPagingApi::class)
class DirectMessagesRemoteMediator @Inject constructor(
private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase
) : RemoteMediator<Int, Conversation>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Conversation>): MediatorResult {
try {
val user = db.userDao().getActiveUser()
?: return MediatorResult.Error(NullPointerException("No active user exists"))
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val nextPage = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> {
// No prepend for the moment, might be nice to add later
return MediatorResult.Success(endOfPaginationReached = true)
}
LoadType.APPEND -> state.lastItemOrNull()?.id?.toIntOrNull()
?.let { state.closestPageToPosition(it) }?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)
}
val apiResponse =
// Pixelfed uses Laravel's paging mechanism for pagination.
//TODO, implement also for Mastodon (see FollowersPagingSource)
api.directMessagesList(
limit = state.config.pageSize.toString(),
page = nextPage
)
apiResponse.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
val endOfPaginationReached = apiResponse.isEmpty()
db.withTransaction {
// Clear table in the database
if (loadType == LoadType.REFRESH) {
db.directMessagesDao().clearFeedContent(user.user_id, user.instance_uri)
}
db.directMessagesDao().insertAll(apiResponse)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: Exception){
return MediatorResult.Error(exception)
}
}
}

View File

@ -24,24 +24,16 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.view.marginEnd
import androidx.core.view.marginTop
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@ -60,9 +52,11 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
import org.pixeldroid.app.login.LoginActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityMainBinding import org.pixeldroid.app.databinding.ActivityMainBinding
import org.pixeldroid.app.directmessages.DirectMessagesActivity
import org.pixeldroid.app.directmessages.DirectMessagesFragment
import org.pixeldroid.app.login.LoginActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.posts.NestedScrollableHost import org.pixeldroid.app.posts.NestedScrollableHost
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
@ -79,7 +73,6 @@ import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.hasInternet import org.pixeldroid.app.utils.hasInternet
import org.pixeldroid.app.utils.loadDefaultMenuTabs
import org.pixeldroid.app.utils.loadDbMenuTabs import org.pixeldroid.app.utils.loadDbMenuTabs
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
@ -260,6 +253,10 @@ class MainActivity : BaseActivity() {
getUpdatedAccount() getUpdatedAccount()
binding.drawer?.itemAdapter?.add( binding.drawer?.itemAdapter?.add(
primaryDrawerItem {
nameRes = R.string.direct_messages
iconRes = R.drawable.message
},
primaryDrawerItem { primaryDrawerItem {
nameRes = R.string.menu_account nameRes = R.string.menu_account
iconRes = R.drawable.person iconRes = R.drawable.person
@ -276,9 +273,10 @@ class MainActivity : BaseActivity() {
binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position -> binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position ->
when (position) { when (position) {
1 -> launchActivity(ProfileActivity()) 1 -> launchActivity(DirectMessagesActivity())
2 -> launchActivity(SettingsActivity()) 2 -> launchActivity(ProfileActivity())
3 -> logOut() 3 -> launchActivity(SettingsActivity())
4 -> logOut()
} }
false false
} }
@ -458,9 +456,7 @@ class MainActivity : BaseActivity() {
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
private fun setupTabs() { private fun setupTabs() {
val tabsCheckedDbEntry = with (db.userDao().getActiveUser()!!) { val tabsCheckedDbEntry = db.tabsDao().getTabsChecked(user!!.user_id, user!!.instance_uri)
db.tabsDao().getTabsChecked(user_id, instance_uri)
}
val pageIds = listOf(R.id.page_1, R.id.page_2, R.id.page_3, R.id.page_4, R.id.page_5) val pageIds = listOf(R.id.page_1, R.id.page_2, R.id.page_3, R.id.page_4, R.id.page_5)
fun Tab.getFragment(): (() -> Fragment) { fun Tab.getFragment(): (() -> Fragment) {
@ -480,33 +476,34 @@ class MainActivity : BaseActivity() {
arguments = Bundle().apply { putBoolean("home", false) } arguments = Bundle().apply { putBoolean("home", false) }
} }
} } } }
Tab.DIRECT_MESSAGES -> {{
DirectMessagesFragment()
}}
} }
} }
val tabs = if (tabsCheckedDbEntry.isEmpty()) { val tabs = if (tabsCheckedDbEntry.isEmpty()) {
// Load default menu // Default menu
loadDefaultMenuTabs(applicationContext, binding.root) Tab.defaultTabs
} else { } else {
// Get current menu visibility and order from settings // Get current menu visibility and order from settings
val tabsChecked = loadDbMenuTabs(applicationContext, tabsCheckedDbEntry).filter { it.second }.map { it.first } loadDbMenuTabs(tabsCheckedDbEntry).filter { it.second }.map { it.first }
}
val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply { val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply {
clear() clear()
} }
?: binding.navigation?.menu?.apply { ?: binding.navigation?.menu?.apply {
removeGroup(R.id.tabsId) if(tabs.contains(Tab.DIRECT_MESSAGES)) removeGroup(R.id.dmNavigationGroup)
} }
tabsChecked.zip(pageIds).forEach { (tabId, pageId) -> tabs.zip(pageIds).forEach { (tabId, pageId) ->
with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) { with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(baseContext))) {
val tabIcon = tabId.getDrawable(applicationContext) val tabIcon = tabId.getDrawable(applicationContext)
if (tabIcon != null) { if (tabIcon != null) {
this?.icon = tabIcon this?.icon = tabIcon
}
} }
} }
tabsChecked
} }
val tabArray: List<() -> Fragment> = tabs.map { it.getFragment() } val tabArray: List<() -> Fragment> = tabs.map { it.getFragment() }
@ -558,6 +555,7 @@ class MainActivity : BaseActivity() {
fun MenuItem.buttonPos() { fun MenuItem.buttonPos() {
when(itemId){ when(itemId){
R.id.dms -> launchActivity(DirectMessagesActivity())
R.id.my_profile -> launchActivity(ProfileActivity()) R.id.my_profile -> launchActivity(ProfileActivity())
R.id.settings -> launchActivity(SettingsActivity()) R.id.settings -> launchActivity(SettingsActivity())
R.id.log_out -> logOut() R.id.log_out -> logOut()

View File

@ -50,7 +50,7 @@ private fun showError(
* Makes the UI respond to various [LoadState]s, including errors when an error message is shown. * Makes the UI respond to various [LoadState]s, including errors when an error message is shown.
*/ */
internal fun <T: Any> initAdapter( internal fun <T: Any> initAdapter(
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout, progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout?,
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding, recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>, adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>,
header: StoriesAdapter? = null header: StoriesAdapter? = null
@ -71,7 +71,7 @@ internal fun <T: Any> initAdapter(
).toTypedArray() ).toTypedArray()
) )
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout?.setOnRefreshListener {
adapter.refresh() adapter.refresh()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
header?.refreshStories() header?.refreshStories()
@ -79,7 +79,7 @@ internal fun <T: Any> initAdapter(
adapter.addLoadStateListener { loadState -> adapter.addLoadStateListener { loadState ->
if(!progressBar.isVisible && swipeRefreshLayout.isRefreshing) { if(!progressBar.isVisible && swipeRefreshLayout?.isRefreshing == true) {
// Stop loading spinner when loading is done // Stop loading spinner when loading is done
swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
} }

View File

@ -1,22 +1,27 @@
package org.pixeldroid.app.posts.feeds.cachedFeeds package org.pixeldroid.app.posts.feeds.cachedFeeds
import android.os.Bundle import android.os.Bundle
import android.util.Log
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 androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
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.LoadState.NotLoading
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.filter
import org.pixeldroid.app.databinding.FragmentFeedBinding import org.pixeldroid.app.databinding.FragmentFeedBinding
import org.pixeldroid.app.directmessages.BounceEdgeEffectFactory
import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.stories.StoriesAdapter import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
@ -67,26 +72,57 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
// } // }
} }
override fun onCreateView( fun createView(inflater: LayoutInflater, container: ViewGroup?,
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, reverseLayout: Boolean = false): ConstraintLayout {
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentFeedBinding.inflate(layoutInflater) binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout, val callback: () -> Unit = {
binding.bottomLoadingBar.visibility = View.VISIBLE
GlobalScope.launch(Dispatchers.Main) {
delay(1000) // Wait 1 second
binding.bottomLoadingBar.visibility = View.GONE
}
adapter.refresh()
adapter.notifyDataSetChanged()
}
val swipeRefreshLayout = if(reverseLayout) {
binding.swipeRefreshLayout.isEnabled = false
binding.list.apply {
layoutManager = LinearLayoutManager(context).apply {
stackFromEnd = false
this.reverseLayout = true
}
edgeEffectFactory = BounceEdgeEffectFactory(callback, context)
}
null
} else binding.swipeRefreshLayout
initAdapter(binding.progressBar, swipeRefreshLayout,
binding.list, binding.motionLayout, binding.errorLayout, adapter, binding.list, binding.motionLayout, binding.errorLayout, adapter,
headerAdapter headerAdapter
) )
return binding.root return binding.root
} }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return createView(inflater, container, savedInstanceState, false)
}
fun onTabReClicked() { fun onTabReClicked() {
binding.list.limitedLengthSmoothScrollToPosition(0) binding.list.limitedLengthSmoothScrollToPosition(0)
} }
private fun onPullUp() {
// Handle the pull-up action
Log.e("bottom", "reached")
}
} }

View File

@ -32,7 +32,8 @@ import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor( class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor(
private val db: AppDatabase, private val db: AppDatabase,
private val dao: FeedContentDao<T>, private val dao: FeedContentDao<T>,
private val mediator: RemoteMediator<Int, T> private val mediator: RemoteMediator<Int, T>,
private val conversationsId: String = "",
) { ) {
/** /**
@ -44,7 +45,7 @@ class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi const
val user = db.userDao().getActiveUser()!! val user = db.userDao().getActiveUser()!!
val pagingSourceFactory = { val pagingSourceFactory = {
dao.feedContent(user.user_id, user.instance_uri) dao.feedContent(user.user_id, user.instance_uri, conversationsId)
} }
return Pager( return Pager(

View File

@ -8,14 +8,10 @@ import androidx.lifecycle.ViewModel
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.LoadState
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.FragmentFeedBinding import org.pixeldroid.app.databinding.FragmentFeedBinding
import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.posts.feeds.launch import org.pixeldroid.app.posts.feeds.launch

View File

@ -1,13 +1,11 @@
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.add
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.fragment.app.replace import androidx.fragment.app.replace
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityFollowersBinding import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
@ -37,7 +35,7 @@ class HashTagActivity : BaseActivity() {
supportFragmentManager.commit { supportFragmentManager.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
replace<UncachedPostsFragment>(R.id.followsFragment, args = arguments) replace<UncachedPostsFragment>(R.id.conversationFragment, args = arguments)
} }
} }
} }

View File

@ -50,7 +50,7 @@ class FollowsActivity : BaseActivity() {
supportFragmentManager.commit { supportFragmentManager.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
replace<AccountListFragment>(R.id.followsFragment, args = arguments) replace<AccountListFragment>(R.id.conversationFragment, args = arguments)
} }
} }
} }

View File

@ -32,14 +32,14 @@ class ArrangeTabsFragment: DialogFragment() {
@Inject @Inject
lateinit var db: AppDatabase lateinit var db: AppDatabase
private val model: ArrangeTabsViewModel by viewModels { ArrangeTabsViewModelFactory(requireContext(), db) } private val model: ArrangeTabsViewModel by viewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
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(dialogView) model.initTabsChecked()
val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs) val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs)
val listAdapter = ListViewAdapter(model) val listAdapter = ListViewAdapter(model)

View File

@ -1,9 +1,7 @@
package org.pixeldroid.app.settings package org.pixeldroid.app.settings
import android.content.Context
import android.view.View
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -11,24 +9,10 @@ import org.pixeldroid.app.utils.Tab
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
import org.pixeldroid.app.utils.loadDbMenuTabs import org.pixeldroid.app.utils.loadDbMenuTabs
import org.pixeldroid.app.utils.loadDefaultMenuTabs import javax.inject.Inject
@HiltViewModel
class ArrangeTabsViewModelFactory( class ArrangeTabsViewModel @Inject constructor(
private val context: Context, private val db: AppDatabase
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArrangeTabsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ArrangeTabsViewModel(context, db) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class ArrangeTabsViewModel(
private val fragmentContext: Context,
private val db: AppDatabase private val db: AppDatabase
): ViewModel() { ): ViewModel() {
@ -56,18 +40,17 @@ class ArrangeTabsViewModel(
} }
} }
fun initTabsChecked(view: View) { fun initTabsChecked() {
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 ->
currentUiState.copy( currentUiState.copy(
tabsChecked = if (_uiState.value.tabsDbEntities.isEmpty()) { tabsChecked = if (_uiState.value.tabsDbEntities.isEmpty()) {
// Load default menu // Load default menu
val list = loadDefaultMenuTabs(fragmentContext, view) Tab.defaultTabs.zip(List(Tab.defaultTabs.size){true}) + Tab.otherTabs.zip(List(Tab.otherTabs.size){false})
list.zip(List(list.size){true}.toTypedArray()).toList()
} else { } else {
// Get current menu visibility and order from settings // Get current menu visibility and order from settings
loadDbMenuTabs(fragmentContext, _uiState.value.tabsDbEntities).toList() loadDbMenuTabs(_uiState.value.tabsDbEntities).toList()
} }
) )
} }

View File

@ -18,7 +18,6 @@ import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.utils.setThemeFromPreferences import org.pixeldroid.app.utils.setThemeFromPreferences
import org.pixeldroid.common.ThemedActivity import org.pixeldroid.common.ThemedActivity
@AndroidEntryPoint @AndroidEntryPoint
class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
@ -41,7 +40,11 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC
// Handle the back button event // Handle the back button event
// If a setting (for example language or theme) was changed, the main activity should be // If a setting (for example language or theme) was changed, the main activity should be
// started without history so that the change is applied to the whole back stack // started without history so that the change is applied to the whole back stack
if (restartMainOnExit) { //TODO restore behaviour without true here, so that MainActivity is not destroyed when not necessary
// The true is a "temporary" (lol) fix so that tab changes are always taken into account
// Also, consider making the up button (arrow in action bar) also take this codepath!
// It recreates the activity by default
if (true || restartMainOnExit) {
val intent = Intent(this@SettingsActivity, MainActivity::class.java) val intent = Intent(this@SettingsActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
super@SettingsActivity.startActivity(intent) super@SettingsActivity.startActivity(intent)

View File

@ -12,14 +12,12 @@ import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.PopupMenu
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
@ -199,23 +197,14 @@ fun <T> Fragment.bindingLifecycleAware(): ReadWriteProperty<Fragment, T> =
} }
} }
fun loadDefaultMenuTabs(context: Context, anchor: View): List<Tab> { fun loadDbMenuTabs(tabsDbEntry: List<TabsDatabaseEntity>): List<Pair<Tab, Boolean>> {
return with(PopupMenu(context, anchor)) {
val menu = this.menu
menuInflater.inflate(R.menu.navigation_main, menu)
menu.removeGroup(R.id.bottomNavigationGroup)
(0 until menu.size()).map { Tab.fromLanguageString(context, menu.getItem(it).title.toString()) }
}
}
fun loadDbMenuTabs(ctx: Context, tabsDbEntry: List<TabsDatabaseEntity>): List<Pair<Tab, Boolean>> {
return tabsDbEntry.map { return tabsDbEntry.map {
Pair(Tab.fromName(it.tab), it.checked) Pair(Tab.fromName(it.tab), it.checked)
} }
} }
enum class Tab { enum class Tab {
HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED; HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES;
fun toLanguageString(ctx: Context): String { fun toLanguageString(ctx: Context): String {
return ctx.getString( return ctx.getString(
@ -225,6 +214,7 @@ enum class Tab {
CREATE_FEED -> R.string.create_feed CREATE_FEED -> R.string.create_feed
NOTIFICATIONS_FEED -> R.string.notifications_feed NOTIFICATIONS_FEED -> R.string.notifications_feed
PUBLIC_FEED -> R.string.public_feed PUBLIC_FEED -> R.string.public_feed
DIRECT_MESSAGES -> R.string.direct_messages
} }
) )
} }
@ -240,6 +230,7 @@ enum class Tab {
CREATE_FEED -> R.drawable.selector_camera CREATE_FEED -> R.drawable.selector_camera
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
} }
return AppCompatResources.getDrawable(ctx, resId) return AppCompatResources.getDrawable(ctx, resId)
} }
@ -252,6 +243,7 @@ enum class Tab {
ctx.getString(R.string.create_feed) -> CREATE_FEED ctx.getString(R.string.create_feed) -> CREATE_FEED
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
else -> HOME_FEED else -> HOME_FEED
} }
} }
@ -259,5 +251,18 @@ enum class Tab {
fun fromName(name: String): Tab { fun fromName(name: String): Tab {
return entries.filter { it.name == name }.getOrElse(0) { HOME_FEED } return entries.filter { it.name == name }.getOrElse(0) { HOME_FEED }
} }
val defaultTabs: List<Tab>
get() = listOf(
HOME_FEED,
SEARCH_DISCOVER_FEED,
CREATE_FEED,
NOTIFICATIONS_FEED,
PUBLIC_FEED
)
val otherTabs: List<Tab>
get() = listOf(
DIRECT_MESSAGES
)
} }
} }

View File

@ -429,6 +429,30 @@ interface PixelfedAPI {
@GET("/api/v1.1/discover/posts/hashtags") @GET("/api/v1.1/discover/posts/hashtags")
suspend fun trendingHashtags() : List<Tag> suspend fun trendingHashtags() : List<Tag>
@GET("/api/v1/conversations")
suspend fun directMessagesList(
// @Query("max_id") max_id: String? = null,
// @Query("since_id") since_id: String? = null,
// @Query("min_id") min_id: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: String? = null,
): List<Conversation>
@GET("/api/v1.1/direct/thread")
suspend fun directMessagesConversation(
@Query("pid") pid: String? = null,
@Query("max_id") max_id: String? = null,
@Query("min_id") min_id: String? = null,
): DMThread
@POST("/api/v1.1/direct/thread/send")
suspend fun sendDirectMessage(
@Query("to_id") to_id: String? = null,
@Query("message") message: String? = null,
// text or emoji
@Query("type") min_id: String = "text",
): DMThread
@FormUrlEncoded @FormUrlEncoded
@POST("/api/v1/reports") @POST("/api/v1/reports")
@JvmSuppressWildcards @JvmSuppressWildcards

View File

@ -0,0 +1,35 @@
package org.pixeldroid.app.utils.api.objects
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import java.io.Serializable
/*
Represents a conversation.
https://docs.joinmastodon.org/entities/Conversation/
*/
@Entity(
tableName = "directMessages",
primaryKeys = ["id", "user_id", "instance_uri"],
foreignKeys = [ForeignKey(
entity = UserDatabaseEntity::class,
parentColumns = arrayOf("user_id", "instance_uri"),
childColumns = arrayOf("user_id", "instance_uri"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["user_id", "instance_uri"])]
)
data class Conversation(
override val id: String,
val unread: Boolean?,
val accounts: List<Account>?,
val last_status: Status?,
//Database values (not from API)
//TODO do we find this approach acceptable? Preferable to a semi-duplicate ConversationDataBaseEntity?
override var user_id: String,
override var instance_uri: String,
): FeedContent, FeedContentDatabase, Serializable

View File

@ -0,0 +1,29 @@
package org.pixeldroid.app.utils.api.objects
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import java.io.Serializable
import java.time.Instant
/*
Represents a conversation.
https://docs.joinmastodon.org/entities/Conversation/
*/
data class DMThread(
override val id: String,
val name: String?,
val username: String?,
val avatar: String?,
val url: String?,
val muted: Boolean?,
val isLocal: Boolean?,
val domain: String?,
val created_at: Instant?, //ISO 8601 Datetime
val updated_at: Instant?,
val timeAgo: String?,
val lastMessage: String?,
val messages: List<Message>,
): FeedContent, Serializable

View File

@ -0,0 +1,19 @@
package org.pixeldroid.app.utils.api.objects
import java.io.Serializable
import java.time.Instant
data class Message(
override val id: String,
val name: String?,
val hidden: Boolean?,
val isAuthor: Boolean?,
val type: String?, //TODO enum?
val text: String?,
val media: String?, //TODO,
val carousel: List<Attachment>?,
val created_at: Instant?, //ISO 8601 Datetime
val timeAgo: String?,
val reportId: String?,
//val meta: String?, //TODO
): FeedContent, Serializable

View File

@ -6,15 +6,21 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import org.pixeldroid.app.utils.db.dao.* import org.pixeldroid.app.utils.api.objects.Conversation
import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.dao.InstanceDao
import org.pixeldroid.app.utils.db.dao.TabsDao
import org.pixeldroid.app.utils.db.dao.UserDao
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesConversationDao
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesDao
import org.pixeldroid.app.utils.db.dao.feedContent.NotificationDao import org.pixeldroid.app.utils.db.dao.feedContent.NotificationDao
import org.pixeldroid.app.utils.db.dao.feedContent.posts.HomePostDao import org.pixeldroid.app.utils.db.dao.feedContent.posts.HomePostDao
import org.pixeldroid.app.utils.db.dao.feedContent.posts.PublicPostDao import org.pixeldroid.app.utils.db.dao.feedContent.posts.PublicPostDao
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
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
import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
@Database(entities = [ @Database(entities = [
@ -23,13 +29,17 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
HomeStatusDatabaseEntity::class, HomeStatusDatabaseEntity::class,
PublicFeedStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class,
Notification::class, Notification::class,
TabsDatabaseEntity::class TabsDatabaseEntity::class,
Conversation::class,
DirectMessageDatabaseEntity::class,
], ],
version = 7,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 6, to = 7) AutoMigration(from = 6, to = 7),
AutoMigration(from = 7, to = 8)
], ],
version = 8
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun instanceDao(): InstanceDao abstract fun instanceDao(): InstanceDao
@ -38,6 +48,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun publicPostDao(): PublicPostDao abstract fun publicPostDao(): PublicPostDao
abstract fun notificationDao(): NotificationDao abstract fun notificationDao(): NotificationDao
abstract fun tabsDao(): TabsDao abstract fun tabsDao(): TabsDao
abstract fun directMessagesDao(): DirectMessagesDao
abstract fun directMessagesConversationDao(): DirectMessagesConversationDao
} }
val MIGRATION_3_4 = object : Migration(3, 4) { val MIGRATION_3_4 = object : Migration(3, 4) {

View File

@ -137,6 +137,15 @@ class Converters {
Status.Visibility::class.java Status.Visibility::class.java
) )
@TypeConverter
fun accountListToJson(type: List<Account>?): String {
val listType = object : TypeToken<List<Account?>?>() {}.type
return gson.toJson(type, listType)
}
@TypeConverter
fun jsonToAccountList(json: String): List<Account>? {
val listType = object : TypeToken<List<Account?>?>() {}.type
return gson.fromJson(json, listType)
}
} }

View File

@ -0,0 +1,25 @@
package org.pixeldroid.app.utils.db.dao.feedContent
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
import org.pixeldroid.app.utils.api.objects.Conversation
import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
@Dao
interface DirectMessagesConversationDao: FeedContentDao<DirectMessageDatabaseEntity> {
@Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId")
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
//TODO think about ordering
@Query("SELECT * FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) DESC")
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, DirectMessageDatabaseEntity>
@Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND conversationsId=:conversationsId")
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
@Query("SELECT id FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) ASC LIMIT 1")
suspend fun lastMessageId(userId: String, instanceUri: String, conversationsId: String): String?
}

View File

@ -0,0 +1,20 @@
package org.pixeldroid.app.utils.db.dao.feedContent
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
import org.pixeldroid.app.utils.api.objects.Conversation
@Dao
interface DirectMessagesDao: FeedContentDao<Conversation> {
@Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
//TODO think about ordering
@Query("""SELECT * FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""""")
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, Conversation>
@Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
}

View File

@ -7,13 +7,14 @@ import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
interface FeedContentDao<T: FeedContentDatabase>{ interface FeedContentDao<T: FeedContentDatabase>{
fun feedContent(userId: String, instanceUri: String): PagingSource<Int, T> fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, T>
suspend fun clearFeedContent(userId: String, instanceUri: String) suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
suspend fun clearFeedContent(userId: String, instanceUri: String) = clearFeedContent(userId, instanceUri, "")
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(feedContent: List<T>) suspend fun insertAll(feedContent: List<T>)
suspend fun delete(id: String, userId: String, instanceUri: String) suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
suspend fun delete(id: String, userId: String, instanceUri: String) = delete(id, userId, instanceUri, "")
} }

View File

@ -8,17 +8,17 @@ import org.pixeldroid.app.utils.api.objects.Notification
@Dao @Dao
interface NotificationDao: FeedContentDao<Notification> { interface NotificationDao: FeedContentDao<Notification> {
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri") @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
override suspend fun clearFeedContent(userId: String, instanceUri: String) override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
ORDER BY datetime(created_at) DESC""") ORDER BY datetime(created_at) DESC""")
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Notification> override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, Notification>
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri @Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri
ORDER BY datetime(created_at) DESC LIMIT 1""") ORDER BY datetime(created_at) DESC LIMIT 1""")
fun latestNotification(userId: String, instanceUri: String): Notification? fun latestNotification(userId: String, instanceUri: String): Notification?
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") @Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
override suspend fun delete(id: String, userId: String, instanceUri: String) override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
} }

View File

@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
@Dao @Dao
interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> { interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> {
@Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri @Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
ORDER BY datetime(created_at) DESC""") ORDER BY datetime(created_at) DESC""")
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, HomeStatusDatabaseEntity> override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, HomeStatusDatabaseEntity>
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri") @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
override suspend fun clearFeedContent(userId: String, instanceUri: String) override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") @Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
override suspend fun delete(id: String, userId: String, instanceUri: String) override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId") @Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean) fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)

View File

@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
@Dao @Dao
interface PublicPostDao: FeedContentDao<PublicFeedStatusDatabaseEntity> { interface PublicPostDao: FeedContentDao<PublicFeedStatusDatabaseEntity> {
@Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri @Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
ORDER BY datetime(created_at) DESC""") ORDER BY datetime(created_at) DESC""")
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, PublicFeedStatusDatabaseEntity> override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, PublicFeedStatusDatabaseEntity>
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri") @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
override suspend fun clearFeedContent(userId: String, instanceUri: String) override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id") @Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
override suspend fun delete(id: String, userId: String, instanceUri: String) override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId") @Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean) fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)

View File

@ -0,0 +1,62 @@
package org.pixeldroid.app.utils.db.entities
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import org.pixeldroid.app.utils.api.objects.Message
import java.io.Serializable
import java.time.Instant
@Entity(
tableName = "directMessagesThreads",
primaryKeys = ["id", "conversationsId", "user_id", "instance_uri"],
foreignKeys = [ForeignKey(
entity = UserDatabaseEntity::class,
parentColumns = arrayOf("user_id", "instance_uri"),
childColumns = arrayOf("user_id", "instance_uri"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["user_id", "instance_uri", "conversationsId"])]
)
data class DirectMessageDatabaseEntity(
override val id: String,
val name: String?,
val hidden: Boolean?,
val isAuthor: Boolean?,
val type: String?, //TODO enum?
val text: String?,
val media: String?, //TODO,
val carousel: List<Attachment>?,
val created_at: Instant?, //ISO 8601 Datetime
val timeAgo: String?,
val reportId: String?,
//val meta: String?, //TODO
// Database values (not from API)
val conversationsId: String,
override var user_id: String,
override var instance_uri: String,
): FeedContent, FeedContentDatabase, Serializable {
constructor(message: Message, conversationsId: String, user: UserDatabaseEntity) : this(
message.id,
message.name,
message.hidden,
message.isAuthor,
message.type,
message.text,
message.media,
message.carousel,
message.created_at,
message.timeAgo,
message.reportId,
//message.meta,
conversationsId,
user.user_id,
user.instance_uri
)
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--Shadow Layers-->
<item>
<rotate
android:fromDegrees="-35"
android:pivotX="0%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp"/>
<padding
android:bottom="1px"
android:left="1px"
android:right="1px"/>
<solid android:color="#01000000" />
</shape>
</rotate>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<padding
android:bottom="1px"
android:left="1px"
android:right="1px"/>
<solid android:color="#01000000" />
<corners android:radius="8dp" />
</shape>
</item>
<!--===============-->
<item>
<rotate
android:fromDegrees="-35"
android:pivotX="0%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp"/>
<padding
android:bottom="1px" />
<solid android:color="#09000000" />
</shape>
</rotate>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<padding
android:bottom="1px" />
<solid android:color="#09000000" />
<corners android:radius="8dp" />
</shape>
</item>
<!--===============-->
<item>
<rotate
android:fromDegrees="-35"
android:pivotX="0%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp"/>
<padding
android:bottom="1px"
android:left="1px"
android:right="1px"/>
<solid android:color="#10000000" />
</shape>
</rotate>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<padding
android:bottom="1px"
android:left="1px"
android:right="1px"/>
<solid android:color="#10000000" />
<corners android:radius="8dp" />
</shape>
</item>
<!--ForeGround-->
<item>
<rotate
android:fromDegrees="-35"
android:pivotX="0%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp"/>
<solid android:color="@color/white" />
</shape>
</rotate>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--Shadow Layer-->
<item>
<rotate
android:fromDegrees="40"
android:pivotX="100%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding
android:bottom="1px"
android:left="1px"
android:right="1px" />
<solid android:color="#01000000" />
</shape>
</rotate>
</item>
<item android:right="10dp">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding
android:bottom="1px"
android:left="1px"
android:right="1px" />
<solid android:color="#01000000" />
</shape>
</item>
<!--===============-->
<item>
<rotate
android:fromDegrees="40"
android:pivotX="100%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding android:bottom="1px" />
<solid android:color="#09000000" />
</shape>
</rotate>
</item>
<item android:right="10dp">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding android:bottom="1px" />
<solid android:color="#09000000" />
</shape>
</item>
<!--===============-->
<item>
<rotate
android:fromDegrees="40"
android:pivotX="100%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding
android:bottom="1px"
android:left="1px"
android:right="1px" />
<solid android:color="#10000000" />
</shape>
</rotate>
</item>
<item android:right="10dp">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<padding
android:bottom="1px"
android:left="1px"
android:right="1px" />
<solid android:color="#10000000" />
</shape>
</item>
<!--===============-->
<!--ForeGround-->
<item>
<rotate
android:fromDegrees="40"
android:pivotX="100%"
android:pivotY="0%"
android:toDegrees="0">
<shape android:shape="rectangle">
<solid android:color="#CBEBFC" />
</shape>
</rotate>
</item>
<item android:right="10dp">
<shape android:shape="rectangle">
<solid android:color="#CBEBFC" />
<corners android:radius="4dp" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M4,4h16v12L5.17,16L4,17.17L4,4m0,-2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2L4,2zM6,12h8v2L6,14v-2zM6,9h12v2L6,11L6,9zM6,6h12v2L6,8L6,6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/message" android:state_checked="true"/>
<item android:drawable="@drawable/outline_message" android:state_checked="false"/>
</selector>

View File

@ -35,7 +35,7 @@
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
app:menu="@menu/navigation_main" /> tools:menu="@menu/navigation_main" />
</LinearLayout> </LinearLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2

View File

@ -38,7 +38,7 @@
app:elevation="0dp" app:elevation="0dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:menu="@menu/navigation_main" /> tools:menu="@menu/navigation_main" />
</LinearLayout> </LinearLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/message_incoming"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/message_bubble_incoming"
android:paddingStart="20dp"
android:paddingTop="4dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp"
app:layout_constraintHeight_max="wrap"
app:layout_constraintHeight_percent="0.8"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="90dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="500dp"
>
<TextView
android:id="@+id/text_message_incoming"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dp"
android:textColor="@color/black"
android:textSize="13.5sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hi, How are you?\nqsdfqsdf" />
<ImageView
android:id="@+id/image_message_incoming"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/message_outgoing"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/message_bubble_outgoing"
android:paddingStart="20dp"
android:paddingTop="4dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
app:layout_constraintHeight_max="wrap"
app:layout_constraintHeight_percent="0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="500dp">
<TextView
android:id="@+id/text_message_outgoing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dp"
android:textColor="@color/black"
android:textSize="13.5sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Fire, and you? 🔥" />
<ImageView
android:id="@+id/image_message_outgoing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
tools:context=".posts.PostActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintPost"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/conversationFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toTopOf="@id/commentIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/conversationFragment"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitComment"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comment_noun"
android:importantForAutofill="no"
android:inputType="text|textCapSentences|textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/submit_comment"
android:text="@string/comment_verb"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -19,7 +19,7 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/followsFragment" android:id="@+id/conversationFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"

View File

@ -37,7 +37,7 @@
app:elevation="0dp" app:elevation="0dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:menu="@menu/navigation_main" /> tools:menu="@menu/navigation_main" />
</LinearLayout> </LinearLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/message_incoming"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/message_bubble_incoming"
android:paddingStart="20dp"
android:paddingTop="4dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp"
app:layout_constraintHeight_max="wrap"
app:layout_constraintHeight_percent="0.8"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="500dp">
<TextView
android:id="@+id/text_message_incoming"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dp"
android:textColor="@color/black"
android:textSize="13.5sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hi, How are you?\nqsdfqsdf" />
<ImageView
android:id="@+id/image_message_incoming"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/message_outgoing"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/message_bubble_outgoing"
android:paddingStart="20dp"
android:paddingTop="4dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
app:layout_constraintHeight_max="wrap"
app:layout_constraintHeight_percent="0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="500dp">
<TextView
android:id="@+id/text_message_outgoing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dp"
android:textColor="@color/black"
android:textSize="13.5sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Fire, and you? 🔥" />
<ImageView
android:id="@+id/image_message_outgoing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_margin="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/dm_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="July 23" />
<TextView
android:id="@+id/dm_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="6dp"
android:paddingStart="38dp"
android:textColor="?android:textColorTertiary"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/message_time"
app:layout_constraintStart_toStartOf="@+id/dm_avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry"
tools:text="fdsqfdsfsqdfdsfqdsfsdfsfddsfqsdsdfsqdf liked your post" />
<ImageView
android:id="@+id/dm_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="14dp"
android:layout_marginTop="14dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dm_username"
tools:src="@drawable/ic_default_user"
android:contentDescription="@string/profile_picture" />
<TextView
android:id="@+id/dm_last_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.164"
app:layout_constraintStart_toEndOf="@+id/dm_avatar"
app:layout_constraintTop_toBottomOf="@+id/dm_username"
app:layout_constraintVertical_bias="0.408"
tools:text="Post description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -57,4 +56,16 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/bottomLoadingBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,33 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/tabsId"> <group
<item android:id="@+id/tabsId">
android:id="@+id/page_1" </group>
android:enabled="true" <group
android:icon="@drawable/selector_home_feed" android:id="@+id/dmNavigationGroup"
android:title="@string/home_feed"/> android:orderInCategory="2">
<item <item
android:id="@+id/page_2" android:id="@+id/dms"
android:enabled="true" android:enabled="true"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/selector_dm"
android:title="@string/search_discover_feed"/> android:title="@string/direct_messages" />
<item </group>
android:id="@+id/page_3" <group
android:enabled="true"
android:icon="@drawable/selector_camera"
android:title="@string/create_feed"/>
<item
android:id="@+id/page_4"
android:enabled="true"
android:icon="@drawable/selector_notifications"
android:title="@string/notifications_feed"/>
<item
android:id="@+id/page_5"
android:enabled="true"
android:icon="@drawable/ic_filter_black_24dp"
android:title="@string/public_feed"/>
</group>
<group
android:id="@+id/bottomNavigationGroup" android:id="@+id/bottomNavigationGroup"
android:orderInCategory="3"> android:orderInCategory="3">
<item <item

View File

@ -1,28 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <group
android:id="@+id/page_1" android:id="@+id/tabsId">
android:enabled="true" </group>
android:icon="@drawable/selector_home_feed"
android:title="@string/home_feed"/>
<item
android:id="@+id/page_2"
android:enabled="true"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/search_discover_feed"/>
<item
android:id="@+id/page_3"
android:enabled="true"
android:icon="@drawable/selector_camera"
android:title="@string/create_feed"/>
<item
android:id="@+id/page_4"
android:enabled="true"
android:icon="@drawable/selector_notifications"
android:title="@string/notifications_feed"/>
<item
android:id="@+id/page_5"
android:enabled="true"
android:icon="@drawable/ic_filter_black_24dp"
android:title="@string/public_feed"/>
</menu> </menu>

View File

@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item type="id" name="comment" /> <item type="id" name="comment" />
<item type="id" name="page_1" />
<item type="id" name="page_2" />
<item type="id" name="page_3" />
<item type="id" name="page_4" />
<item type="id" name="page_5" />
</resources> </resources>

View File

@ -351,4 +351,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="arrange_tabs_summary">Arrange tabs</string> <string name="arrange_tabs_summary">Arrange tabs</string>
<string name="arrange_tabs_description">Change visibility and order of tabs</string> <string name="arrange_tabs_description">Change visibility and order of tabs</string>
<string name="content_header">Content</string> <string name="content_header">Content</string>
<string name="dm_title">DM to %1$s</string>
<string name="direct_messages">Direct Messages</string>
</resources> </resources>