Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
ba005c769b
|
@ -90,15 +90,19 @@ android {
|
|||
enableSplit = false
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
}
|
||||
|
||||
ext.coroutinesVersion = "1.6.0"
|
||||
ext.lifecycleVersion = "2.3.1"
|
||||
ext.roomVersion = '2.3.0'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.1'
|
||||
ext.okhttpVersion = '4.9.3'
|
||||
ext.glideVersion = '4.12.0'
|
||||
ext.daggerVersion = '2.37'
|
||||
ext.materialdrawerVersion = '8.4.1'
|
||||
ext.daggerVersion = '2.40.5'
|
||||
ext.materialdrawerVersion = '8.4.5'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
|
@ -110,8 +114,8 @@ repositories {
|
|||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.5.0"
|
||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
||||
|
@ -119,7 +123,7 @@ dependencies {
|
|||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
|
@ -129,7 +133,7 @@ dependencies {
|
|||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation "androidx.work:work-runtime:2.5.0"
|
||||
|
@ -137,7 +141,9 @@ dependencies {
|
|||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
implementation "com.google.android.material:material:1.3.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
|
||||
implementation "com.google.code.gson:gson:2.8.9"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
|
@ -152,7 +158,7 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.12.0"
|
||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0"
|
||||
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
|
@ -173,23 +179,23 @@ dependencies {
|
|||
|
||||
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar'
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:3.1.0"
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
||||
|
||||
implementation "de.c1710:filemojicompat:1.0.18"
|
||||
|
||||
testImplementation "androidx.test.ext:junit:1.1.2"
|
||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
|
||||
implementation 'net.accelf:easter:1.0.2'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
|
|
@ -33,6 +33,10 @@
|
|||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# TUSKY SPECIFIC OPTIONS
|
||||
|
||||
# keep members of our model classes, they are used in json de/serialization
|
||||
|
@ -43,10 +47,37 @@
|
|||
public *;
|
||||
}
|
||||
|
||||
-keepclassmembers class com.keylesspalace.tusky.components.conversation.ConversationAccountEntity { *; }
|
||||
-keepclassmembers class com.keylesspalace.tusky.db.DraftAttachment { *; }
|
||||
|
||||
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
|
||||
public *;
|
||||
}
|
||||
|
||||
# https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
# Retain generic signatures of classes used in MastodonApi so Retrofit works
|
||||
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.collections.List
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.collections.Map
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Call
|
||||
|
||||
# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#retrofit
|
||||
-keepattributes Signature
|
||||
-keep class kotlin.coroutines.Continuation
|
||||
|
||||
# preserve line numbers for crash reporting
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
|
|
@ -0,0 +1,789 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 29,
|
||||
"identityHash": "62c289344334da2db091ad4ba0a49c6a",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "DraftEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToId",
|
||||
"columnName": "inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentWarning",
|
||||
"columnName": "contentWarning",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "failedToSend",
|
||||
"columnName": "failedToSend",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "AccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domain",
|
||||
"columnName": "domain",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profilePictureUrl",
|
||||
"columnName": "profilePictureUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsEnabled",
|
||||
"columnName": "notificationsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsMentioned",
|
||||
"columnName": "notificationsMentioned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFollowed",
|
||||
"columnName": "notificationsFollowed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFollowRequested",
|
||||
"columnName": "notificationsFollowRequested",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsReblogged",
|
||||
"columnName": "notificationsReblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFavorited",
|
||||
"columnName": "notificationsFavorited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsPolls",
|
||||
"columnName": "notificationsPolls",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsSubscriptions",
|
||||
"columnName": "notificationsSubscriptions",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationSound",
|
||||
"columnName": "notificationSound",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationVibration",
|
||||
"columnName": "notificationVibration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationLight",
|
||||
"columnName": "notificationLight",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultPostPrivacy",
|
||||
"columnName": "defaultPostPrivacy",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultMediaSensitivity",
|
||||
"columnName": "defaultMediaSensitivity",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alwaysShowSensitiveMedia",
|
||||
"columnName": "alwaysShowSensitiveMedia",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alwaysOpenSpoiler",
|
||||
"columnName": "alwaysOpenSpoiler",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaPreviewEnabled",
|
||||
"columnName": "mediaPreviewEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastNotificationId",
|
||||
"columnName": "lastNotificationId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activeNotifications",
|
||||
"columnName": "activeNotifications",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tabPreferences",
|
||||
"columnName": "tabPreferences",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFilter",
|
||||
"columnName": "notificationsFilter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_AccountEntity_domain_accountId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"domain",
|
||||
"accountId"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "InstanceEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "instance",
|
||||
"columnName": "instance",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojiList",
|
||||
"columnName": "emojiList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maximumTootCharacters",
|
||||
"columnName": "maximumTootCharacters",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPollOptions",
|
||||
"columnName": "maxPollOptions",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPollOptionLength",
|
||||
"columnName": "maxPollOptionLength",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"instance"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TimelineStatusEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorServerId",
|
||||
"columnName": "authorServerId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToId",
|
||||
"columnName": "inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToAccountId",
|
||||
"columnName": "inReplyToAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogsCount",
|
||||
"columnName": "reblogsCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favouritesCount",
|
||||
"columnName": "favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoilerText",
|
||||
"columnName": "spoilerText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogServerId",
|
||||
"columnName": "reblogServerId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogAccountId",
|
||||
"columnName": "reblogAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expanded",
|
||||
"columnName": "expanded",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentCollapsed",
|
||||
"columnName": "contentCollapsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentShowing",
|
||||
"columnName": "contentShowing",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"authorServerId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "TimelineAccountEntity",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"authorServerId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "TimelineAccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localUsername",
|
||||
"columnName": "localUsername",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar",
|
||||
"columnName": "avatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bot",
|
||||
"columnName": "bot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "ConversationEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.id",
|
||||
"columnName": "s_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.url",
|
||||
"columnName": "s_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToId",
|
||||
"columnName": "s_inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||
"columnName": "s_inReplyToAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.account",
|
||||
"columnName": "s_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.content",
|
||||
"columnName": "s_content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.createdAt",
|
||||
"columnName": "s_createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.emojis",
|
||||
"columnName": "s_emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favouritesCount",
|
||||
"columnName": "s_favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favourited",
|
||||
"columnName": "s_favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.bookmarked",
|
||||
"columnName": "s_bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.sensitive",
|
||||
"columnName": "s_sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.spoilerText",
|
||||
"columnName": "s_spoilerText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.attachments",
|
||||
"columnName": "s_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.mentions",
|
||||
"columnName": "s_mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.tags",
|
||||
"columnName": "s_tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.showingHiddenContent",
|
||||
"columnName": "s_showingHiddenContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.expanded",
|
||||
"columnName": "s_expanded",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.collapsible",
|
||||
"columnName": "s_collapsible",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.collapsed",
|
||||
"columnName": "s_collapsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.muted",
|
||||
"columnName": "s_muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.poll",
|
||||
"columnName": "s_poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id",
|
||||
"accountId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"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, '62c289344334da2db091ad4ba0a49c6a')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,807 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 30,
|
||||
"identityHash": "a75615171612bdfc9e3d4201ebf6071a",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "DraftEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToId",
|
||||
"columnName": "inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentWarning",
|
||||
"columnName": "contentWarning",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "failedToSend",
|
||||
"columnName": "failedToSend",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "AccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domain",
|
||||
"columnName": "domain",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profilePictureUrl",
|
||||
"columnName": "profilePictureUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsEnabled",
|
||||
"columnName": "notificationsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsMentioned",
|
||||
"columnName": "notificationsMentioned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFollowed",
|
||||
"columnName": "notificationsFollowed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFollowRequested",
|
||||
"columnName": "notificationsFollowRequested",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsReblogged",
|
||||
"columnName": "notificationsReblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFavorited",
|
||||
"columnName": "notificationsFavorited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsPolls",
|
||||
"columnName": "notificationsPolls",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsSubscriptions",
|
||||
"columnName": "notificationsSubscriptions",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationSound",
|
||||
"columnName": "notificationSound",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationVibration",
|
||||
"columnName": "notificationVibration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationLight",
|
||||
"columnName": "notificationLight",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultPostPrivacy",
|
||||
"columnName": "defaultPostPrivacy",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultMediaSensitivity",
|
||||
"columnName": "defaultMediaSensitivity",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alwaysShowSensitiveMedia",
|
||||
"columnName": "alwaysShowSensitiveMedia",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alwaysOpenSpoiler",
|
||||
"columnName": "alwaysOpenSpoiler",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mediaPreviewEnabled",
|
||||
"columnName": "mediaPreviewEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastNotificationId",
|
||||
"columnName": "lastNotificationId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activeNotifications",
|
||||
"columnName": "activeNotifications",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tabPreferences",
|
||||
"columnName": "tabPreferences",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFilter",
|
||||
"columnName": "notificationsFilter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_AccountEntity_domain_accountId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"domain",
|
||||
"accountId"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "InstanceEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "instance",
|
||||
"columnName": "instance",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojiList",
|
||||
"columnName": "emojiList",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maximumTootCharacters",
|
||||
"columnName": "maximumTootCharacters",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPollOptions",
|
||||
"columnName": "maxPollOptions",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPollOptionLength",
|
||||
"columnName": "maxPollOptionLength",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minPollDuration",
|
||||
"columnName": "minPollDuration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPollDuration",
|
||||
"columnName": "maxPollDuration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "charactersReservedPerUrl",
|
||||
"columnName": "charactersReservedPerUrl",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"instance"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TimelineStatusEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorServerId",
|
||||
"columnName": "authorServerId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToId",
|
||||
"columnName": "inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReplyToAccountId",
|
||||
"columnName": "inReplyToAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogsCount",
|
||||
"columnName": "reblogsCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favouritesCount",
|
||||
"columnName": "favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoilerText",
|
||||
"columnName": "spoilerText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogServerId",
|
||||
"columnName": "reblogServerId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogAccountId",
|
||||
"columnName": "reblogAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expanded",
|
||||
"columnName": "expanded",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentCollapsed",
|
||||
"columnName": "contentCollapsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentShowing",
|
||||
"columnName": "contentShowing",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"authorServerId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "TimelineAccountEntity",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"authorServerId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "TimelineAccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localUsername",
|
||||
"columnName": "localUsername",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar",
|
||||
"columnName": "avatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bot",
|
||||
"columnName": "bot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "ConversationEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.id",
|
||||
"columnName": "s_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.url",
|
||||
"columnName": "s_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToId",
|
||||
"columnName": "s_inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||
"columnName": "s_inReplyToAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.account",
|
||||
"columnName": "s_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.content",
|
||||
"columnName": "s_content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.createdAt",
|
||||
"columnName": "s_createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.emojis",
|
||||
"columnName": "s_emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favouritesCount",
|
||||
"columnName": "s_favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favourited",
|
||||
"columnName": "s_favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.bookmarked",
|
||||
"columnName": "s_bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.sensitive",
|
||||
"columnName": "s_sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.spoilerText",
|
||||
"columnName": "s_spoilerText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.attachments",
|
||||
"columnName": "s_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.mentions",
|
||||
"columnName": "s_mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.tags",
|
||||
"columnName": "s_tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.showingHiddenContent",
|
||||
"columnName": "s_showingHiddenContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.expanded",
|
||||
"columnName": "s_expanded",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.collapsible",
|
||||
"columnName": "s_collapsible",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.collapsed",
|
||||
"columnName": "s_collapsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.muted",
|
||||
"columnName": "s_muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.poll",
|
||||
"columnName": "s_poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id",
|
||||
"accountId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"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, 'a75615171612bdfc9e3d4201ebf6071a')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -112,7 +112,7 @@
|
|||
android:name=".ViewMediaActivity"
|
||||
android:theme="@style/TuskyBaseTheme" />
|
||||
<activity
|
||||
android:name=".AccountActivity"
|
||||
android:name=".components.account.AccountActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||
<activity android:name=".EditProfileActivity" />
|
||||
<activity android:name=".components.preference.PreferencesActivity" />
|
||||
|
|
|
@ -197,6 +197,33 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
|||
.show();
|
||||
}
|
||||
|
||||
public @Nullable String getOpenAsText() {
|
||||
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
||||
switch (accounts.size()) {
|
||||
case 0:
|
||||
case 1:
|
||||
return null;
|
||||
case 2:
|
||||
for (AccountEntity account : accounts) {
|
||||
if (account != accountManager.getActiveAccount()) {
|
||||
return String.format(getString(R.string.action_open_as), account.getFullName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return String.format(getString(R.string.action_open_as), "…");
|
||||
}
|
||||
}
|
||||
|
||||
public void openAsAccount(@NonNull String url, @NonNull AccountEntity account) {
|
||||
accountManager.setActiveAccount(account);
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra(MainActivity.REDIRECT_URL, url);
|
||||
startActivity(intent);
|
||||
finishWithoutSlideOutAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
@ -25,8 +26,9 @@ import androidx.lifecycle.Lifecycle
|
|||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
@ -163,9 +165,9 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
||||
open fun openLink(url: String) {
|
||||
LinkHelper.openLink(url, this)
|
||||
(this as Context).openLink(url)
|
||||
}
|
||||
|
||||
private fun showQuerySheet() {
|
||||
|
@ -186,6 +188,8 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
// https://friendica.foo.bar/profile/user
|
||||
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
|
||||
// https://pixelfed.social/p/connyduck/391263492998670833
|
||||
// https://pixelfed.social/connyduck
|
||||
// https://mastodon.foo.bar/users/User/statuses/000000000000000000
|
||||
fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||
val uri: URI
|
||||
|
@ -211,6 +215,8 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
|
|||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
|
||||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
|
||||
path.matches("^/profile/\\w+$".toRegex()) ||
|
||||
path.matches("^/p/\\w+/\\d+$".toRegex()) ||
|
||||
path.matches("^/\\w+$".toRegex()) ||
|
||||
path.matches("^/users/[^/]+/statuses/\\d+$".toRegex())
|
||||
}
|
||||
|
||||
|
|
|
@ -15,28 +15,26 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.canhub.cropper.CropImage
|
||||
import com.canhub.cropper.CropImageContract
|
||||
import com.canhub.cropper.options
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||
|
@ -44,9 +42,7 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
|
@ -63,12 +59,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
const val HEADER_WIDTH = 1500
|
||||
const val HEADER_HEIGHT = 500
|
||||
|
||||
private const val AVATAR_PICK_RESULT = 1
|
||||
private const val HEADER_PICK_RESULT = 2
|
||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
private const val MAX_ACCOUNT_FIELDS = 4
|
||||
|
||||
private const val BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING"
|
||||
}
|
||||
|
||||
@Inject
|
||||
|
@ -78,23 +69,28 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||
|
||||
private var currentlyPicking: PickType = PickType.NOTHING
|
||||
|
||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||
|
||||
private enum class PickType {
|
||||
NOTHING,
|
||||
AVATAR,
|
||||
HEADER
|
||||
}
|
||||
|
||||
private val cropImage = registerForActivityResult(CropImageContract()) { result ->
|
||||
if (result.isSuccessful) {
|
||||
if (result.uriContent == viewModel.getAvatarUri()) {
|
||||
viewModel.newAvatarPicked()
|
||||
} else {
|
||||
viewModel.newHeaderPicked()
|
||||
}
|
||||
} else {
|
||||
onPickFailure(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let {
|
||||
currentlyPicking = PickType.valueOf(it)
|
||||
}
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
@ -104,8 +100,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
||||
binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
||||
binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) }
|
||||
binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) }
|
||||
|
||||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.fieldList.adapter = accountFieldEditAdapter
|
||||
|
@ -159,51 +155,44 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
is Error -> {
|
||||
val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
snackbar.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
snackbar.show()
|
||||
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
is Loading -> { }
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.obtainInstance()
|
||||
viewModel.instanceData.observe(this) { result ->
|
||||
when (result) {
|
||||
is Success -> {
|
||||
val instance = result.data
|
||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
||||
}
|
||||
if (result is Success) {
|
||||
val instance = result.data
|
||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
||||
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, false)
|
||||
|
||||
viewModel.saveData.observe(
|
||||
this,
|
||||
{
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
this
|
||||
) {
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -219,89 +208,60 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
private fun observeImage(
|
||||
liveData: LiveData<Resource<Bitmap>>,
|
||||
liveData: LiveData<Uri>,
|
||||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean
|
||||
) {
|
||||
liveData.observe(
|
||||
this,
|
||||
{
|
||||
this
|
||||
) { imageUri ->
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
val glide = Glide.with(imageView)
|
||||
.load(it.data)
|
||||
// skipping all caches so we can always reuse the same uri
|
||||
val glide = Glide.with(imageView)
|
||||
.load(imageUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
}
|
||||
|
||||
glide.into(imageView)
|
||||
|
||||
imageView.show()
|
||||
progressBar.hide()
|
||||
}
|
||||
is Loading -> {
|
||||
progressBar.show()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if (!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
).into(imageView)
|
||||
} else {
|
||||
glide.into(imageView)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onMediaPick(pickType: PickType) {
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
// Ignore inputs if another pick operation is still occurring.
|
||||
return
|
||||
}
|
||||
currentlyPicking = pickType
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
initiateMediaPicking()
|
||||
imageView.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
initiateMediaPicking()
|
||||
} else {
|
||||
endMediaPicking()
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initiateMediaPicking() {
|
||||
private fun pickMedia(pickType: PickType) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "image/*"
|
||||
when (currentlyPicking) {
|
||||
when (pickType) {
|
||||
PickType.AVATAR -> {
|
||||
startActivityForResult(intent, AVATAR_PICK_RESULT)
|
||||
cropImage.launch(
|
||||
options {
|
||||
setRequestedSize(AVATAR_SIZE, AVATAR_SIZE)
|
||||
setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||
setImageSource(includeGallery = true, includeCamera = false)
|
||||
setOutputUri(viewModel.getAvatarUri())
|
||||
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
}
|
||||
)
|
||||
}
|
||||
PickType.HEADER -> {
|
||||
startActivityForResult(intent, HEADER_PICK_RESULT)
|
||||
cropImage.launch(
|
||||
options {
|
||||
setRequestedSize(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
setImageSource(includeGallery = true, includeCamera = false)
|
||||
setOutputUri(viewModel.getHeaderUri())
|
||||
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
}
|
||||
)
|
||||
}
|
||||
PickType.NOTHING -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,16 +281,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
private fun save() {
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.save(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -340,90 +295,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
binding.saveProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun beginMediaPicking() {
|
||||
when (currentlyPicking) {
|
||||
PickType.AVATAR -> {
|
||||
binding.avatarProgressBar.visibility = View.VISIBLE
|
||||
binding.avatarPreview.visibility = View.INVISIBLE
|
||||
binding.avatarButton.setImageDrawable(null)
|
||||
}
|
||||
PickType.HEADER -> {
|
||||
binding.headerProgressBar.visibility = View.VISIBLE
|
||||
binding.headerPreview.visibility = View.INVISIBLE
|
||||
binding.headerButton.setImageDrawable(null)
|
||||
}
|
||||
PickType.NOTHING -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
private fun endMediaPicking() {
|
||||
binding.avatarProgressBar.visibility = View.GONE
|
||||
binding.headerProgressBar.visibility = View.GONE
|
||||
|
||||
currentlyPicking = PickType.NOTHING
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
AVATAR_PICK_RESULT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||
.start(this)
|
||||
} else {
|
||||
endMediaPicking()
|
||||
}
|
||||
}
|
||||
HEADER_PICK_RESULT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
.start(this)
|
||||
} else {
|
||||
endMediaPicking()
|
||||
}
|
||||
}
|
||||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
||||
val result = CropImage.getActivityResult(data)
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
||||
else -> endMediaPicking()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun beginResize(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
currentlyPicking = PickType.NOTHING
|
||||
return
|
||||
}
|
||||
|
||||
beginMediaPicking()
|
||||
|
||||
when (currentlyPicking) {
|
||||
PickType.AVATAR -> {
|
||||
viewModel.newAvatar(uri, this)
|
||||
}
|
||||
PickType.HEADER -> {
|
||||
viewModel.newHeader(uri, this)
|
||||
}
|
||||
else -> {
|
||||
throw AssertionError("PickType not set.")
|
||||
}
|
||||
}
|
||||
|
||||
currentlyPicking = PickType.NOTHING
|
||||
}
|
||||
|
||||
private fun onResizeFailure() {
|
||||
private fun onPickFailure(throwable: Throwable?) {
|
||||
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
endMediaPicking()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ import at.connyduck.sparkbutton.helpers.Utils
|
|||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
|
@ -201,7 +200,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
private fun onListSelected(listId: String) {
|
||||
startActivityWithSlideInAnimation(
|
||||
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
|
||||
StatusListActivity.newListIntent(this, listId)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,14 @@ import com.bumptech.glide.request.transition.Transition
|
|||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||
import com.keylesspalace.tusky.appstore.CacheUpdater
|
||||
import com.keylesspalace.tusky.appstore.Event
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||
|
@ -83,7 +90,14 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
|||
import com.keylesspalace.tusky.interfaces.ResettableFragment
|
||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.removeShortcut
|
||||
import com.keylesspalace.tusky.util.updateShortcut
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
@ -92,9 +106,25 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle
|
|||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.util.*
|
||||
import com.mikepenz.materialdrawer.model.AbstractDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.addItems
|
||||
import com.mikepenz.materialdrawer.util.addItemsAtPosition
|
||||
import com.mikepenz.materialdrawer.util.addStickyDrawerItems
|
||||
import com.mikepenz.materialdrawer.util.updateBadge
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
|
@ -356,9 +386,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
if (intent != null) {
|
||||
val statusUrl = intent.getStringExtra(STATUS_URL)
|
||||
if (statusUrl != null) {
|
||||
viewUrl(statusUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
||||
val redirectUrl = intent.getStringExtra(REDIRECT_URL)
|
||||
if (redirectUrl != null) {
|
||||
viewUrl(redirectUrl, PostLookupFallbackBehavior.DISPLAY_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -991,7 +1021,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private const val TAG = "MainActivity" // logging tag
|
||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||
const val STATUS_URL = "statusUrl"
|
||||
const val REDIRECT_URL = "redirectUrl"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import net.accelf.yuito.QuickTootViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
||||
|
||||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityModalTimelineBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
supportActionBar?.apply {
|
||||
title = getString(R.string.title_list_timeline)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
|
||||
?: TimelineViewModel.Kind.HOME
|
||||
val argument = intent?.getStringExtra(ARG_ARG)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
||||
.commit()
|
||||
}
|
||||
|
||||
binding.viewQuickToot.attachViewModel(quickTootViewModel, this)
|
||||
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(binding.viewQuickToot::handleEvent)
|
||||
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
|
||||
}
|
||||
|
||||
override fun getActionButton(): FloatingActionButton? = null
|
||||
|
||||
override fun androidInjector() = dispatchingAndroidInjector
|
||||
|
||||
companion object {
|
||||
private const val ARG_KIND = "kind"
|
||||
private const val ARG_ARG = "arg"
|
||||
|
||||
@JvmStatic
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
kind: TimelineViewModel.Kind,
|
||||
argument: String?
|
||||
): Intent {
|
||||
val intent = Intent(context, ModalTimelineActivity::class.java)
|
||||
intent.putExtra(ARG_KIND, kind)
|
||||
intent.putExtra(ARG_ARG, argument)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,9 +44,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
private val quickTootViewModel: QuickTootViewModel by viewModels{ viewModelFactory }
|
||||
|
||||
private val kind: Kind
|
||||
get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityStatuslistBinding.inflate(layoutInflater)
|
||||
|
@ -54,10 +51,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
||||
val title = if (kind == Kind.FAVOURITES) {
|
||||
R.string.title_favourites
|
||||
} else {
|
||||
R.string.title_bookmarks
|
||||
val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
|
||||
val listId = intent.getStringExtra(EXTRA_LIST_ID)
|
||||
val hashtag = intent.getStringExtra(EXTRA_HASHTAG)
|
||||
|
||||
val title = when (kind) {
|
||||
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
|
||||
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
|
||||
else -> getString(R.string.title_list_timeline)
|
||||
}
|
||||
|
||||
supportActionBar?.run {
|
||||
|
@ -66,9 +68,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
supportFragmentManager.commit {
|
||||
val fragment = TimelineFragment.newInstance(kind)
|
||||
replace(R.id.fragment_container, fragment)
|
||||
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
|
||||
supportFragmentManager.commit {
|
||||
val fragment = if (kind == Kind.TAG) {
|
||||
TimelineFragment.newHashtagInstance(listOf(hashtag!!))
|
||||
} else {
|
||||
TimelineFragment.newInstance(kind, listId)
|
||||
}
|
||||
replace(R.id.fragmentContainer, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.viewQuickToot.attachViewModel(quickTootViewModel, this)
|
||||
|
@ -85,17 +93,30 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
companion object {
|
||||
|
||||
private const val EXTRA_KIND = "kind"
|
||||
private const val EXTRA_LIST_ID = "id"
|
||||
private const val EXTRA_HASHTAG = "tag"
|
||||
|
||||
@JvmStatic
|
||||
fun newFavouritesIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newBookmarksIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||
}
|
||||
|
||||
fun newListIntent(context: Context, listId: String) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.LIST.name)
|
||||
putExtra(EXTRA_LIST_ID, listId)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newHashtagIntent(context: Context, hashtag: String) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.TAG.name)
|
||||
putExtra(EXTRA_HASHTAG, hashtag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.HasAndroidInjector;
|
||||
|
||||
public class ViewTagActivity extends BottomSheetActivity implements HasAndroidInjector {
|
||||
|
||||
private static final String HASHTAG = "hashtag";
|
||||
|
||||
@Inject
|
||||
public DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
|
||||
|
||||
public static Intent getIntent(Context context, String tag){
|
||||
Intent intent = new Intent(context,ViewTagActivity.class);
|
||||
intent.putExtra(HASHTAG,tag);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_view_tag);
|
||||
|
||||
String hashtag = getIntent().getStringExtra(HASHTAG);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar bar = getSupportActionBar();
|
||||
|
||||
if (bar != null) {
|
||||
bar.setTitle(String.format(getString(R.string.title_tag), hashtag));
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
bar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
Fragment fragment = TimelineFragment.newHashtagInstance(Collections.singletonList(hashtag));
|
||||
fragmentTransaction.replace(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Object> androidInjector() {
|
||||
return dispatchingAndroidInjector;
|
||||
}
|
||||
|
||||
}
|
|
@ -111,7 +111,7 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_open_in_web: {
|
||||
LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
|
||||
openLink(getIntent().getStringExtra(URL_EXTRA));
|
||||
return true;
|
||||
}
|
||||
case R.id.action_reveal: {
|
||||
|
|
|
@ -663,7 +663,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener);
|
||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), statusViewData.getActionable().getTags(), listener);
|
||||
|
||||
CharSequence emojifiedContentWarning;
|
||||
if (statusViewData.getSpoilerText() != null) {
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.entity.Attachment.Focus;
|
|||
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CardViewMode;
|
||||
|
@ -211,6 +212,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable List<Status.Mention> mentions,
|
||||
@Nullable List<HashTag> tags,
|
||||
@NonNull List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
|
@ -231,13 +233,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
setContentWarningButtonText(!expanded);
|
||||
|
||||
this.setTextVisible(sensitive, !expanded, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
||||
this.setTextVisible(sensitive, !expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||
});
|
||||
this.setTextVisible(sensitive, expanded, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
||||
this.setTextVisible(sensitive, expanded, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||
} else {
|
||||
contentWarningDescription.setVisibility(View.GONE);
|
||||
contentWarningButton.setVisibility(View.GONE);
|
||||
this.setTextVisible(sensitive, true, content, mentions, emojis, poll, statusDisplayOptions, listener);
|
||||
this.setTextVisible(sensitive, true, content, mentions, tags, emojis, poll, statusDisplayOptions, listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,13 +255,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
boolean expanded,
|
||||
Spanned content,
|
||||
List<Status.Mention> mentions,
|
||||
List<HashTag> tags,
|
||||
List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
final StatusActionListener listener) {
|
||||
if (expanded) {
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
|
||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, tags, listener);
|
||||
for (int i = 0; i < mediaLabels.length; ++i) {
|
||||
updateMediaLabel(i, sensitive, expanded);
|
||||
}
|
||||
|
@ -879,7 +882,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
if (cardView != null) {
|
||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
|
||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
|
||||
}
|
||||
|
||||
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
||||
|
@ -888,7 +891,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setQuoteEnabled(actionable.rebloggingAllowed() && !actionable.isNotestock(), actionable.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
actionable.getMentions(), actionable.getEmojis(),
|
||||
actionable.getMentions(), actionable.getTags(), actionable.getEmojis(),
|
||||
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
||||
listener);
|
||||
|
||||
|
@ -1143,7 +1146,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
||||
}
|
||||
|
||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
|
||||
protected void setupCard(
|
||||
StatusViewData.Concrete status,
|
||||
CardViewMode cardViewMode,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
final StatusActionListener listener
|
||||
) {
|
||||
final Card card = status.getActionable().getCard();
|
||||
if (cardViewMode != CardViewMode.NONE &&
|
||||
status.getActionable().getAttachments().size() == 0 &&
|
||||
|
@ -1234,7 +1242,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
||||
}
|
||||
|
||||
View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext());
|
||||
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl(), "");
|
||||
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
||||
|
||||
cardInfo.setOnClickListener(visitLink);
|
||||
|
|
|
@ -110,7 +110,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
|
||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||
if (payloads == null) {
|
||||
|
||||
if (!statusDisplayOptions.hideStats()) {
|
||||
|
|
|
@ -3,15 +3,12 @@ package com.keylesspalace.tusky.appstore
|
|||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
private val accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
gson: Gson
|
||||
) {
|
||||
|
@ -21,11 +18,6 @@ class CacheUpdater @Inject constructor(
|
|||
init {
|
||||
val timelineDao = appDatabase.timelineDao()
|
||||
|
||||
Schedulers.io().scheduleDirect {
|
||||
val olderThan = System.currentTimeMillis() - CLEANUP_INTERVAL
|
||||
appDatabase.timelineDao().cleanup(olderThan)
|
||||
}
|
||||
|
||||
disposable = eventHub.events.subscribe { event ->
|
||||
val accountId = accountManager.activeAccount?.id ?: return@subscribe
|
||||
when (event) {
|
||||
|
@ -53,16 +45,7 @@ class CacheUpdater @Inject constructor(
|
|||
this.disposable.dispose()
|
||||
}
|
||||
|
||||
fun clearForUser(accountId: Long) {
|
||||
Single.fromCallable {
|
||||
appDatabase.timelineDao().removeAllForAccount(accountId)
|
||||
appDatabase.timelineDao().removeAllUsersForAccount(accountId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
|
||||
suspend fun clearForUser(accountId: Long) {
|
||||
appDatabase.timelineDao().removeAll(accountId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,19 @@ package com.keylesspalace.tusky.appstore
|
|||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface Event
|
||||
interface Dispatchable : Event
|
||||
|
||||
interface EventHub {
|
||||
val events: Observable<Event>
|
||||
fun dispatch(event: Dispatchable)
|
||||
}
|
||||
|
||||
object EventHubImpl : EventHub {
|
||||
@Singleton
|
||||
class EventHub @Inject constructor() {
|
||||
|
||||
private val eventsSubject = PublishSubject.create<Event>()
|
||||
override val events: Observable<Event> = eventsSubject
|
||||
val events: Observable<Event> = eventsSubject
|
||||
|
||||
override fun dispatch(event: Dispatchable) {
|
||||
fun dispatch(event: Dispatchable) {
|
||||
eventsSubject.onNext(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.content.Context
|
||||
|
@ -51,30 +51,39 @@ import com.google.android.material.shape.ShapeAppearanceModel
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.EditProfileActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import java.text.NumberFormat
|
||||
|
@ -258,8 +267,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
toolbarParams.topMargin = top
|
||||
|
||||
val right = insets.getInsets(systemBars()).right
|
||||
val bottom = insets.getInsets(systemBars()).bottom
|
||||
binding.accountCoordinatorLayout.updatePadding(bottom = bottom)
|
||||
val left = insets.getInsets(systemBars()).left
|
||||
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
@ -353,6 +364,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
is Loading -> { }
|
||||
}
|
||||
}
|
||||
viewModel.relationshipData.observe(this) {
|
||||
|
@ -404,7 +416,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||
|
||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
|
||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||
|
@ -446,7 +458,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.into(binding.accountHeaderImageView)
|
||||
|
||||
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
val intent =
|
||||
ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
avatarView.transitionName = account.avatar
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||
|
@ -511,7 +524,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
if (account.isRemote()) {
|
||||
binding.accountRemoveView.show()
|
||||
binding.accountRemoveView.setOnClickListener {
|
||||
LinkHelper.openLink(account.url, this)
|
||||
openLink(account.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -674,6 +687,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.account_toolbar, menu)
|
||||
|
||||
val openAsItem = menu.findItem(R.id.action_open_as)
|
||||
val title = openAsText
|
||||
if (title == null) {
|
||||
openAsItem.isVisible = false
|
||||
} else {
|
||||
openAsItem.title = title
|
||||
}
|
||||
|
||||
if (!viewModel.isSelf) {
|
||||
|
||||
val block = menu.findItem(R.id.action_block)
|
||||
|
@ -692,7 +713,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
if (loadedAccount != null) {
|
||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||
domain = LinkHelper.getDomain(loadedAccount?.url)
|
||||
domain = getDomain(loadedAccount?.url)
|
||||
if (domain.isEmpty()) {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
|
@ -793,8 +814,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
|
@ -812,11 +832,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
when (item.itemId) {
|
||||
R.id.action_open_in_web -> {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
if (loadedAccount != null) {
|
||||
LinkHelper.openLink(loadedAccount?.url, this)
|
||||
if (loadedAccount?.url != null) {
|
||||
openLink(loadedAccount!!.url)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_open_as -> {
|
||||
if (loadedAccount != null) {
|
||||
showAccountChooserDialog(
|
||||
item.title, false,
|
||||
object : AccountSelectionListener {
|
||||
override fun onAccountSelected(account: AccountEntity) {
|
||||
openAsAccount(loadedAccount!!.url, account)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
R.id.action_block -> {
|
||||
toggleBlock()
|
||||
return true
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
|
@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.IdentityProof
|
|||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.createClickableText
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
|
||||
class AccountFieldAdapter(
|
||||
private val linkListener: LinkListener,
|
||||
|
@ -54,7 +55,7 @@ class AccountFieldAdapter(
|
|||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
nameTextView.text = identityProof.provider
|
||||
valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
|
||||
valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
|
||||
|
||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
|
@ -65,7 +66,7 @@ class AccountFieldAdapter(
|
|||
nameTextView.text = emojifiedName
|
||||
|
||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
|
||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||
|
||||
if (field.verifiedAt != null) {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
|
@ -13,13 +13,13 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.pager
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.fragment.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||
import com.keylesspalace.tusky.util.CustomFragmentStateAdapter
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
package com.keylesspalace.tusky.components.account.media
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
|
@ -37,9 +37,9 @@ import com.keylesspalace.tusky.entity.Attachment
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.SquareImageView
|
||||
|
@ -252,7 +252,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
}
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
LinkHelper.openLink(items[currentIndex].attachment.url, context)
|
||||
context?.openLink(items[currentIndex].attachment.url)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,21 +15,25 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.announcements
|
||||
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.size
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
||||
import com.keylesspalace.tusky.entity.Announcement
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.EmojiSpan
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
interface AnnouncementActionListener : LinkListener {
|
||||
fun openReactionPicker(announcementId: String, target: View)
|
||||
|
@ -56,7 +60,9 @@ class AnnouncementAdapter(
|
|||
val chips = holder.binding.chipGroup
|
||||
val addReactionChip = holder.binding.addReactionChip
|
||||
|
||||
LinkHelper.setClickableText(text, item.content, null, listener)
|
||||
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
||||
|
||||
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
||||
|
||||
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
||||
if (wellbeingEnabled) {
|
||||
|
@ -67,42 +73,43 @@ class AnnouncementAdapter(
|
|||
}
|
||||
|
||||
item.reactions.forEachIndexed { i, reaction ->
|
||||
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
||||
isCheckable = true
|
||||
checkedIcon = null
|
||||
chips.addView(this, i)
|
||||
}
|
||||
.apply {
|
||||
val emojiText = if (reaction.url == null) {
|
||||
reaction.name
|
||||
} else {
|
||||
context.getString(R.string.emoji_shortcode_format, reaction.name)
|
||||
(
|
||||
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
||||
isCheckable = true
|
||||
checkedIcon = null
|
||||
chips.addView(this, i)
|
||||
}
|
||||
)
|
||||
.apply {
|
||||
if (reaction.url == null) {
|
||||
this.text = "${reaction.name} ${reaction.count}"
|
||||
} else {
|
||||
// we set the EmojiSpan on a space, because otherwise the Chip won't have the right size
|
||||
// https://github.com/tuskyapp/Tusky/issues/2308
|
||||
val spanBuilder = SpannableStringBuilder(" ${reaction.count}")
|
||||
val span = EmojiSpan(WeakReference(this))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
span.contentDescription = reaction.name
|
||||
}
|
||||
this.text = ("$emojiText ${reaction.count}")
|
||||
.emojify(
|
||||
listOf(
|
||||
Emoji(
|
||||
reaction.name,
|
||||
reaction.url ?: "",
|
||||
reaction.staticUrl ?: "",
|
||||
null
|
||||
)
|
||||
),
|
||||
this,
|
||||
animateEmojis
|
||||
)
|
||||
spanBuilder.setSpan(span, 0, 1, 0)
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
|
||||
.into(span.getTarget(animateEmojis))
|
||||
this.text = spanBuilder
|
||||
}
|
||||
|
||||
isChecked = reaction.me
|
||||
isChecked = reaction.me
|
||||
|
||||
setOnClickListener {
|
||||
if (reaction.me) {
|
||||
listener.removeReaction(item.id, reaction.name)
|
||||
} else {
|
||||
listener.addReaction(item.id, reaction.name)
|
||||
}
|
||||
setOnClickListener {
|
||||
if (reaction.me) {
|
||||
listener.removeReaction(item.id, reaction.name)
|
||||
} else {
|
||||
listener.addReaction(item.id, reaction.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (chips.size - 1 > item.reactions.size) {
|
||||
|
|
|
@ -27,7 +27,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
||||
|
@ -152,22 +152,17 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
|
|||
viewModel.removeReaction(announcementId, name)
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String?) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = StatusListActivity.newHashtagIntent(this, tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String?) {
|
||||
if (id != null) {
|
||||
viewAccount(id)
|
||||
}
|
||||
override fun onViewAccount(id: String) {
|
||||
viewAccount(id)
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String?, text: String?) {
|
||||
if (url != null) {
|
||||
viewUrl(url)
|
||||
}
|
||||
override fun onViewUrl(url: String, text: String) {
|
||||
viewUrl(url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.InstanceEntity
|
||||
|
@ -57,19 +58,21 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
},
|
||||
{ emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().maxTootChars,
|
||||
either.asRight().pollLimits?.maxOptions,
|
||||
either.asRight().pollLimits?.maxOptionChars,
|
||||
either.asRight().version
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars,
|
||||
either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions,
|
||||
either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars,
|
||||
either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration,
|
||||
either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration,
|
||||
either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
||||
either.asRight().version
|
||||
)
|
||||
}
|
||||
.doOnSuccess {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
|
|
|
@ -77,7 +77,20 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.afterTextChanged
|
||||
import com.keylesspalace.tusky.util.combineLiveData
|
||||
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
@ -115,6 +128,7 @@ class ComposeActivity :
|
|||
|
||||
@VisibleForTesting
|
||||
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
||||
var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH
|
||||
|
||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
||||
|
||||
|
@ -357,6 +371,7 @@ class ComposeActivity :
|
|||
withLifecycleContext {
|
||||
viewModel.instanceParams.observe { instanceData ->
|
||||
maximumTootCharacters = instanceData.maxChars
|
||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||
updateVisibleCharactersLeft()
|
||||
binding.composeScheduleButton.visible(instanceData.supportsScheduled)
|
||||
}
|
||||
|
@ -715,7 +730,8 @@ class ComposeActivity :
|
|||
val instanceParams = viewModel.instanceParams.value!!
|
||||
showAddPollDialog(
|
||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||
instanceParams.pollMaxLength, viewModel::updatePoll
|
||||
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
|
||||
viewModel::updatePoll
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -760,7 +776,7 @@ class ComposeActivity :
|
|||
val urlSpans = binding.composeEditField.urls
|
||||
if (urlSpans != null) {
|
||||
for (span in urlSpans) {
|
||||
offset += max(0, span.url.length - MAXIMUM_URL_LENGTH)
|
||||
offset += max(0, span.url.length - charactersReservedPerUrl)
|
||||
}
|
||||
}
|
||||
var length = binding.composeEditField.length() - offset
|
||||
|
@ -1114,10 +1130,6 @@ class ComposeActivity :
|
|||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||
|
||||
// Mastodon only counts URLs as this long in terms of status character limits
|
||||
@VisibleForTesting
|
||||
const val MAXIMUM_URL_LENGTH = 23
|
||||
|
||||
@JvmField
|
||||
val CAN_USE_UNLEAKABLE = arrayOf("itabashi.0j0.jp", "odakyu.app")
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
|
@ -82,6 +82,9 @@ class ComposeViewModel @Inject constructor(
|
|||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||
pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
||||
pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
||||
charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
||||
)
|
||||
}
|
||||
|
@ -107,18 +110,20 @@ class ComposeViewModel @Inject constructor(
|
|||
fun loadInstanceDataFromNetwork(loadActually: Boolean) {
|
||||
when (loadActually) {
|
||||
true -> Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance(),
|
||||
{ emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = domain,
|
||||
emojiList = emojis,
|
||||
maximumTootCharacters = instance.maxTootChars,
|
||||
maxPollOptions = instance.pollLimits?.maxOptions,
|
||||
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
|
||||
version = instance.version
|
||||
)
|
||||
}
|
||||
)
|
||||
api.getCustomEmojis(), api.getInstance()
|
||||
) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
emojiList = emojis,
|
||||
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
|
||||
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
|
||||
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
|
||||
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
||||
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
||||
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
||||
version = instance.version
|
||||
)
|
||||
}
|
||||
false -> Single.error(Exception("skipped network access"))
|
||||
}
|
||||
.doOnSuccess {
|
||||
|
@ -192,7 +197,7 @@ class ComposeViewModel @Inject constructor(
|
|||
is UploadEvent.ProgressEvent ->
|
||||
item.copy(uploadPercent = event.percentage)
|
||||
is UploadEvent.FinishedEvent ->
|
||||
item.copy(id = event.attachment.id, uploadPercent = -1)
|
||||
item.copy(id = event.mediaId, uploadPercent = -1)
|
||||
}
|
||||
synchronized(media) {
|
||||
val mediaValue = media.value!!
|
||||
|
@ -520,7 +525,12 @@ fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = defau
|
|||
|
||||
const val DEFAULT_CHARACTER_LIMIT = 500
|
||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||
private const val DEFAULT_MAX_OPTION_LENGTH = 25
|
||||
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
||||
private const val DEFAULT_MIN_POLL_DURATION = 300
|
||||
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
||||
|
||||
// Mastodon only counts URLs as this long in terms of status character limits
|
||||
const val DEFAULT_MAXIMUM_URL_LENGTH = 23
|
||||
|
||||
val CAN_USE_QUOTE_ID = arrayOf("odakyu.app", "itabashi.0j0.jp", "biwakodon.com", "dtp-mstdn.jp", "nitiasa.com",
|
||||
"comm.cx", "fedibird.com", "qoto.org", "kurage.cc", "m.eula.dev", "otogamer.me", "sgp.hostdon.ne.jp",
|
||||
|
@ -530,6 +540,9 @@ data class ComposeInstanceParams(
|
|||
val maxChars: Int,
|
||||
val pollMaxOptions: Int,
|
||||
val pollMaxLength: Int,
|
||||
val pollMinDuration: Int,
|
||||
val pollMaxDuration: Int,
|
||||
val charactersReservedPerUrl: Int,
|
||||
val supportsScheduled: Boolean
|
||||
)
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ import androidx.core.net.toUri
|
|||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||
|
@ -41,10 +40,11 @@ import java.io.File
|
|||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed class UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||
data class FinishedEvent(val attachment: Attachment) : UploadEvent()
|
||||
data class FinishedEvent(val mediaId: String) : UploadEvent()
|
||||
}
|
||||
|
||||
fun createNewImageFile(context: Context): File {
|
||||
|
@ -61,21 +61,16 @@ fun createNewImageFile(context: Context): File {
|
|||
|
||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||
|
||||
interface MediaUploader {
|
||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia>
|
||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent>
|
||||
}
|
||||
|
||||
class AudioSizeException : Exception()
|
||||
class VideoSizeException : Exception()
|
||||
class MediaTypeException : Exception()
|
||||
class CouldNotOpenFileException : Exception()
|
||||
|
||||
class MediaUploaderImpl(
|
||||
class MediaUploader @Inject constructor(
|
||||
private val context: Context,
|
||||
private val mastodonApi: MastodonApi
|
||||
) : MediaUploader {
|
||||
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||
) {
|
||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||
return Observable
|
||||
.fromCallable {
|
||||
if (shouldResizeMedia(media)) {
|
||||
|
@ -86,7 +81,7 @@ class MediaUploaderImpl(
|
|||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
override fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||
return Single.fromCallable {
|
||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||
var uri = inUri
|
||||
|
@ -187,14 +182,14 @@ class MediaUploaderImpl(
|
|||
|
||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||
.subscribe(
|
||||
{ attachment ->
|
||||
{ result ->
|
||||
if (media.uri.scheme == "file") {
|
||||
media.uri.path?.let {
|
||||
File(it).delete()
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||
emitter.onNext(UploadEvent.FinishedEvent(result.id))
|
||||
emitter.onComplete()
|
||||
},
|
||||
{ e ->
|
||||
|
|
|
@ -20,6 +20,7 @@ package com.keylesspalace.tusky.components.compose.dialog
|
|||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.DialogAddPollBinding
|
||||
|
@ -30,6 +31,8 @@ fun showAddPollDialog(
|
|||
poll: NewPoll?,
|
||||
maxOptionCount: Int,
|
||||
maxOptionLength: Int,
|
||||
minDuration: Int,
|
||||
maxDuration: Int,
|
||||
onUpdatePoll: (NewPoll) -> Unit
|
||||
) {
|
||||
|
||||
|
@ -57,6 +60,13 @@ fun showAddPollDialog(
|
|||
|
||||
binding.pollChoices.adapter = adapter
|
||||
|
||||
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
|
||||
val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
||||
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
|
||||
setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
|
||||
}
|
||||
durations = durations.filter { it in minDuration..maxDuration }
|
||||
|
||||
binding.addChoiceButton.setOnClickListener {
|
||||
if (adapter.itemCount < maxOptionCount) {
|
||||
adapter.addChoice()
|
||||
|
@ -66,7 +76,7 @@ fun showAddPollDialog(
|
|||
}
|
||||
}
|
||||
|
||||
val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
||||
val pollDurationId = durations.indexOfLast {
|
||||
it <= poll?.expiresIn ?: 0
|
||||
}
|
||||
|
||||
|
@ -79,13 +89,10 @@ fun showAddPollDialog(
|
|||
button.setOnClickListener {
|
||||
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
|
||||
|
||||
val pollDuration = context.resources
|
||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||
|
||||
onUpdatePoll(
|
||||
NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
expiresIn = durations[selectedPollDurationId],
|
||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||
)
|
||||
)
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.keylesspalace.tusky.entity.Account
|
|||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
|
@ -79,6 +80,7 @@ data class ConversationStatusEntity(
|
|||
val spoilerText: String,
|
||||
val attachments: ArrayList<Attachment>,
|
||||
val mentions: List<Status.Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
|
@ -107,6 +109,7 @@ data class ConversationStatusEntity(
|
|||
if (spoilerText != other.spoilerText) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (mentions != other.mentions) return false
|
||||
if (tags != other.tags) return false
|
||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
|
@ -132,6 +135,7 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
|
@ -162,6 +166,7 @@ data class ConversationStatusEntity(
|
|||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = muted,
|
||||
|
@ -198,6 +203,7 @@ fun Status.toEntity() =
|
|||
spoilerText = spoilerText,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
showingHiddenContent = false,
|
||||
expanded = false,
|
||||
collapsible = shouldTrimStatus(content),
|
||||
|
|
|
@ -108,7 +108,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
false, statusDisplayOptions);
|
||||
|
||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
status.getMentions(), status.getEmojis(),
|
||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||
|
||||
setConversationName(conversation.getAccounts());
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -30,9 +29,9 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.compose.CAN_USE_QUOTE_ID
|
||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -53,6 +52,7 @@ import kotlinx.coroutines.launch
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
||||
|
||||
@Inject
|
||||
|
@ -73,7 +73,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
|
||||
|
@ -239,8 +238,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(context, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
val intent = StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import androidx.paging.RemoteMediator
|
|||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class ConversationsRemoteMediator(
|
||||
private val accountId: Long,
|
||||
private val api: MastodonApi,
|
||||
|
|
|
@ -37,7 +37,7 @@ class ConversationsViewModel @Inject constructor(
|
|||
private val api: MastodonApi
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
val conversationFlow = Pager(
|
||||
config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20),
|
||||
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
|
@ -12,7 +13,7 @@ import javax.inject.Inject
|
|||
class NotificationFetcher @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val notifier: Notifier
|
||||
private val context: Context
|
||||
) {
|
||||
fun fetchAndShow() {
|
||||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||
|
@ -20,7 +21,7 @@ class NotificationFetcher @Inject constructor(
|
|||
try {
|
||||
val notifications = fetchNotifications(account)
|
||||
notifications.forEachIndexed { index, notification ->
|
||||
notifier.show(notification, account, index == 0)
|
||||
NotificationHelper.make(context, notification, account, index == 0)
|
||||
}
|
||||
accountManager.saveAccount(account)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
|
||||
/**
|
||||
* Shows notifications.
|
||||
*/
|
||||
interface Notifier {
|
||||
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
|
||||
}
|
||||
|
||||
class SystemNotifier(
|
||||
private val context: Context
|
||||
) : Notifier {
|
||||
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
|
||||
NotificationHelper.make(context, notification, account, isFirstInBatch)
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.StatusViewHelper
|
||||
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
||||
|
@ -33,6 +33,8 @@ import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
|
|||
import com.keylesspalace.tusky.util.TimestampUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.setClickableMentions
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.toViewData
|
||||
|
@ -96,7 +98,7 @@ class StatusViewHolder(
|
|||
)
|
||||
|
||||
if (status.spoilerText.isBlank()) {
|
||||
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler, status.quote != null)
|
||||
setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
binding.statusContentWarningButton.hide()
|
||||
binding.statusContentWarningDescription.hide()
|
||||
} else {
|
||||
|
@ -110,13 +112,11 @@ class StatusViewHolder(
|
|||
val contentShown = viewState.isContentShow(status.id, true)
|
||||
binding.statusContentWarningDescription.invalidate()
|
||||
viewState.setContentShow(status.id, !contentShown)
|
||||
setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler,
|
||||
status.quote != null)
|
||||
setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
setContentWarningButtonText(!contentShown)
|
||||
}
|
||||
}
|
||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler,
|
||||
status.quote != null)
|
||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler, status.quote != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,16 +132,17 @@ class StatusViewHolder(
|
|||
private fun setTextVisible(
|
||||
expanded: Boolean,
|
||||
content: Spanned,
|
||||
mentions: List<Status.Mention>?,
|
||||
mentions: List<Status.Mention>,
|
||||
tags: List<HashTag>?,
|
||||
emojis: List<Emoji>,
|
||||
listener: LinkListener,
|
||||
removeQuote: Boolean,
|
||||
) {
|
||||
if (expanded) {
|
||||
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
|
||||
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)
|
||||
setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(binding.statusContent, mentions, listener)
|
||||
setClickableMentions(binding.statusContent, mentions, listener)
|
||||
}
|
||||
if (binding.statusContent.text.isNullOrBlank()) {
|
||||
binding.statusContent.hide()
|
||||
|
|
|
@ -28,10 +28,10 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.compose.CAN_USE_QUOTE_ID
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
|
@ -182,9 +182,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
|||
|
||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||
|
||||
override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
||||
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
|
||||
override fun onViewUrl(url: String?, text: String?) = viewModel.checkClickedUrl(url)
|
||||
override fun onViewUrl(url: String, text: String) = viewModel.checkClickedUrl(url)
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ReportStatusesFragment()
|
||||
|
|
|
@ -81,7 +81,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
}
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
if (loadState.refresh is Error) {
|
||||
if (loadState.refresh is LoadState.Error) {
|
||||
binding.progressBar.hide()
|
||||
binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
refreshStatuses()
|
||||
|
|
|
@ -42,7 +42,7 @@ class SearchViewModel @Inject constructor(
|
|||
mastodonApi: MastodonApi,
|
||||
notestockApi: NotestockApi,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val accountManager: AccountManager
|
||||
private val accountManager: AccountManager,
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
var currentQuery: String = ""
|
||||
|
@ -58,14 +58,19 @@ class SearchViewModel @Inject constructor(
|
|||
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
||||
val quoteEnabled = activeAccount?.domain in CAN_USE_QUOTE_ID
|
||||
|
||||
private val loadedStatuses: MutableList<Pair<Status, StatusViewData.Concrete>> = mutableListOf()
|
||||
private val loadedNotestockStatuses: MutableList<Pair<Status, StatusViewData.Concrete>> = mutableListOf()
|
||||
private val loadedStatuses: MutableList<StatusViewData.Concrete> = mutableListOf()
|
||||
private val loadedNotestockStatuses: MutableList<StatusViewData.Concrete> = mutableListOf()
|
||||
|
||||
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
||||
it.statuses.map { status -> Pair(status, status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler, true)) }
|
||||
.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
it.statuses.map { status ->
|
||||
status.toViewData(
|
||||
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||
isExpanded = alwaysOpenSpoiler,
|
||||
isCollapsed = true
|
||||
)
|
||||
}.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
}
|
||||
private val accountsPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Account) {
|
||||
it.accounts
|
||||
|
@ -74,7 +79,13 @@ class SearchViewModel @Inject constructor(
|
|||
it.hashtags
|
||||
}
|
||||
private val notestockStatusesPagingSourceFactory = SearchNotestockPagingSourceFactory(notestockApi) {
|
||||
it.statuses.map { status -> Pair(status, status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler, true)) }
|
||||
it.statuses.map { status ->
|
||||
status.toViewData(
|
||||
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||
isExpanded = alwaysOpenSpoiler,
|
||||
isCollapsed = true
|
||||
)
|
||||
}
|
||||
.apply {
|
||||
loadedNotestockStatuses.addAll(this)
|
||||
}
|
||||
|
@ -112,126 +123,125 @@ class SearchViewModel @Inject constructor(
|
|||
notestockStatusesPagingSourceFactory.newSearch(query)
|
||||
}
|
||||
|
||||
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
|
||||
timelineCases.delete(status.first.id)
|
||||
fun removeItem(statusViewData: StatusViewData.Concrete) {
|
||||
timelineCases.delete(statusViewData.id)
|
||||
.subscribe(
|
||||
{
|
||||
if (loadedStatuses.remove(status))
|
||||
if (loadedStatuses.remove(statusViewData))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
},
|
||||
{
|
||||
err ->
|
||||
{ err ->
|
||||
Log.d(TAG, "Failed to delete status", err)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun removeNotestockItem(status: Pair<Status, StatusViewData.Concrete>) {
|
||||
if (loadedNotestockStatuses.remove(status))
|
||||
fun removeNotestockItem(statusViewData: StatusViewData.Concrete) {
|
||||
if (loadedNotestockStatuses.remove(statusViewData)) {
|
||||
notestockStatusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun expandedNotestockChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(status)
|
||||
fun expandedNotestockChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedNotestockStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
loadedNotestockStatuses[idx] = statusViewData.copy(isExpanded = expanded)
|
||||
notestockStatusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun reblog(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
timelineCases.reblog(status.first.id, reblog)
|
||||
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||
timelineCases.reblog(statusViewData.id, reblog)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ setRebloggedForStatus(status, reblog) },
|
||||
{ t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) }
|
||||
{ setRebloggedForStatus(statusViewData, reblog) },
|
||||
{ t -> Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun setRebloggedForStatus(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
status.first.reblogged = reblog
|
||||
status.first.reblog?.reblogged = reblog
|
||||
private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
||||
statusViewData.status.reblogged = reblog
|
||||
statusViewData.status.reblog?.reblogged = reblog
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
|
||||
fun contentHiddenChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun contentHiddenNotestockChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(status)
|
||||
fun contentHiddenNotestockChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedNotestockStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
loadedNotestockStatuses[idx] = statusViewData.copy(isShowingContent = isShowing)
|
||||
notestockStatusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun collapsedChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun collapsedNotestockChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(status)
|
||||
fun collapsedNotestockChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
||||
val idx = loadedNotestockStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
loadedNotestockStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
loadedNotestockStatuses[idx] = statusViewData.copy(isCollapsed = collapsed)
|
||||
notestockStatusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun voteInPoll(status: Pair<Status, StatusViewData.Concrete>, choices: MutableList<Int>) {
|
||||
val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices)
|
||||
updateStatus(status, votedPoll)
|
||||
timelineCases.voteInPoll(status.first.id, votedPoll.id, choices)
|
||||
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
||||
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
||||
updateStatus(statusViewData, votedPoll)
|
||||
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ newPoll -> updateStatus(status, newPoll) },
|
||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) }
|
||||
{ newPoll -> updateStatus(statusViewData, newPoll) },
|
||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun updateStatus(status: Pair<Status, StatusViewData.Concrete>, newPoll: Poll) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
val newStatus = status.first.copy(poll = newPoll)
|
||||
val newViewData = status.second.copy(status = newStatus)
|
||||
loadedStatuses[idx] = Pair(newStatus, newViewData)
|
||||
val newStatus = statusViewData.status.copy(poll = newPoll)
|
||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun favorite(status: Pair<Status, StatusViewData.Concrete>, isFavorited: Boolean) {
|
||||
status.first.favourited = isFavorited
|
||||
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
||||
statusViewData.status.favourited = isFavorited
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.favourite(status.first.id, isFavorited)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.favourite(statusViewData.id, isFavorited)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
|
||||
status.first.bookmarked = isBookmarked
|
||||
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
||||
statusViewData.status.bookmarked = isBookmarked
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.bookmark(status.first.id, isBookmarked)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
@ -256,19 +266,15 @@ class SearchViewModel @Inject constructor(
|
|||
return timelineCases.delete(id)
|
||||
}
|
||||
|
||||
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(statusViewData)
|
||||
if (idx >= 0) {
|
||||
val newStatus = status.first.copy(muted = mute)
|
||||
val newPair = Pair(
|
||||
newStatus,
|
||||
status.second.copy(status = newStatus)
|
||||
)
|
||||
loadedStatuses[idx] = newPair
|
||||
val newStatus = statusViewData.status.copy(muted = mute)
|
||||
loadedStatuses[idx] = statusViewData.copy(status = newStatus)
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
timelineCases.muteConversation(status.first.id, mute)
|
||||
.onErrorReturnItem(status.first)
|
||||
timelineCases.muteConversation(statusViewData.id, mute)
|
||||
.onErrorReturnItem(statusViewData.status)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
@ -13,19 +12,19 @@ import java.util.*
|
|||
class SearchNotestockPagingSource(
|
||||
private val notestockApi: NotestockApi,
|
||||
private val searchRequest: String,
|
||||
private val initialItems: List<Pair<Status, StatusViewData.Concrete>>? = null,
|
||||
private val parser: (SearchResult) -> List<Pair<Status, StatusViewData.Concrete>>
|
||||
) : PagingSource<Date, Pair<Status, StatusViewData.Concrete>>() {
|
||||
private val initialItems: List<StatusViewData.Concrete>? = null,
|
||||
private val parser: (SearchResult) -> List<StatusViewData.Concrete>
|
||||
) : PagingSource<Date, StatusViewData.Concrete>() {
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val iso8601 = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
|
||||
.apply { timeZone = UTC }
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Date, Pair<Status, StatusViewData.Concrete>>): Date? {
|
||||
override fun getRefreshKey(state: PagingState<Date, StatusViewData.Concrete>): Date? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Date>): LoadResult<Date, Pair<Status, StatusViewData.Concrete>> {
|
||||
override suspend fun load(params: LoadParams<Date>): LoadResult<Date, StatusViewData.Concrete> {
|
||||
if (searchRequest.isEmpty()) {
|
||||
return LoadResult.Page(
|
||||
data = emptyList(),
|
||||
|
@ -38,7 +37,7 @@ class SearchNotestockPagingSource(
|
|||
return LoadResult.Page(
|
||||
data = initialItems.toList(),
|
||||
prevKey = null,
|
||||
nextKey = initialItems.last().first.createdAt
|
||||
nextKey = initialItems.last().status.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -53,7 +52,7 @@ class SearchNotestockPagingSource(
|
|||
return LoadResult.Page(
|
||||
data = res,
|
||||
prevKey = null,
|
||||
nextKey = res.last().first.createdAt,
|
||||
nextKey = res.last().status.createdAt,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package com.keylesspalace.tusky.components.search.adapter
|
||||
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
class SearchNotestockPagingSourceFactory(
|
||||
private val notestockApi: NotestockApi,
|
||||
private val initialItems: List<Pair<Status, StatusViewData.Concrete>>? = null,
|
||||
private val parser: (SearchResult) -> List<Pair<Status, StatusViewData.Concrete>>
|
||||
private val initialItems: List<StatusViewData.Concrete>? = null,
|
||||
private val parser: (SearchResult) -> List<StatusViewData.Concrete>
|
||||
) : () -> SearchNotestockPagingSource {
|
||||
|
||||
private var searchRequest: String = ""
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.PagingDataAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -29,7 +28,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
|
|||
class SearchStatusesAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusListener: StatusActionListener
|
||||
) : PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
|
@ -39,22 +38,18 @@ class SearchStatusesAdapter(
|
|||
|
||||
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
||||
getItem(position)?.let { item ->
|
||||
holder.setupWithStatus(item.second, statusListener, statusDisplayOptions)
|
||||
holder.setupWithStatus(item, statusListener, statusDisplayOptions)
|
||||
}
|
||||
}
|
||||
|
||||
fun item(position: Int): Pair<Status, StatusViewData.Concrete>? {
|
||||
return getItem(position)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Pair<Status, StatusViewData.Concrete>>() {
|
||||
override fun areContentsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
oldItem.second.id == newItem.second.id
|
||||
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -113,7 +113,7 @@ abstract class SearchFragment<T : Any> :
|
|||
|
||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||
|
||||
override fun onViewTag(tag: String) = startActivity(ViewTagActivity.getIntent(requireContext(), tag))
|
||||
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
|
||||
override fun onViewUrl(url: String, text: String) {
|
||||
bottomSheetActivity?.viewUrl(url, text = text)
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
|
@ -46,15 +45,15 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
|
||||
class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener {
|
||||
|
||||
override val data: Flow<PagingData<Pair<Status, StatusViewData.Concrete>>>
|
||||
override val data: Flow<PagingData<StatusViewData.Concrete>>
|
||||
get() = viewModel.notestockStatusesFlow
|
||||
|
||||
private val searchAdapter
|
||||
get() = super.adapter as SearchStatusesAdapter
|
||||
|
||||
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
||||
override fun createAdapter(): PagingDataAdapter<StatusViewData.Concrete, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
|
@ -77,13 +76,13 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.contentHiddenNotestockChange(it, isShowing)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
reply(status)
|
||||
}
|
||||
}
|
||||
|
@ -93,25 +92,25 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
}
|
||||
|
||||
override fun onQuote(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
quote(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.bookmark(status, bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
searchAdapter.item(position)?.first?.let {
|
||||
searchAdapter.peek(position)?.status?.let {
|
||||
more(it, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
searchAdapter.item(position)?.first?.actionableStatus?.let { actionable ->
|
||||
searchAdapter.peek(position)?.status?.actionableStatus?.let { actionable ->
|
||||
when (actionable.attachments[attachmentIndex].type) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
|
@ -140,20 +139,20 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
val actionableStatus = status.actionableStatus
|
||||
bottomSheetActivity?.viewUrl(actionableStatus.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
bottomSheetActivity?.viewAccount(status.account.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.expandedNotestockChange(it, expanded)
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +162,7 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.collapsedNotestockChange(it, isCollapsed)
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +172,7 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
}
|
||||
|
||||
fun removeItem(position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.removeNotestockItem(it)
|
||||
}
|
||||
}
|
||||
|
@ -348,7 +347,7 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_mute_conversation -> {
|
||||
searchAdapter.item(position)?.let { foundStatus ->
|
||||
searchAdapter.peek(position)?.let { foundStatus ->
|
||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
|
@ -420,21 +419,12 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
|
|||
false,
|
||||
object : AccountSelectionListener {
|
||||
override fun onAccountSelected(account: AccountEntity) {
|
||||
openAsAccount(statusUrl, account)
|
||||
bottomSheetActivity?.openAsAccount(statusUrl, account)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun openAsAccount(statusUrl: String, account: AccountEntity) {
|
||||
viewModel.activeAccount = account
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl)
|
||||
startActivity(intent)
|
||||
(activity as BaseActivity).finishWithoutSlideOutAnimation()
|
||||
}
|
||||
|
||||
private fun downloadAllMedia(status: Status) {
|
||||
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
|
||||
for ((_, url) in status.attachments) {
|
||||
|
|
|
@ -40,7 +40,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
|
@ -55,23 +54,23 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
|
||||
class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener {
|
||||
|
||||
override val data: Flow<PagingData<Pair<Status, StatusViewData.Concrete>>>
|
||||
override val data: Flow<PagingData<StatusViewData.Concrete>>
|
||||
get() = viewModel.statusesFlow
|
||||
|
||||
private val searchAdapter
|
||||
get() = super.adapter as SearchStatusesAdapter
|
||||
|
||||
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
||||
override fun createAdapter(): PagingDataAdapter<StatusViewData.Concrete, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
|
@ -93,43 +92,43 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.contentHiddenChange(it, isShowing)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
reply(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.favorite(status, favourite)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQuote(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
quote(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.bookmark(status, bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
searchAdapter.item(position)?.first?.let {
|
||||
searchAdapter.peek(position)?.status?.let {
|
||||
more(it, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
searchAdapter.item(position)?.first?.actionableStatus?.let { actionable ->
|
||||
searchAdapter.peek(position)?.status?.actionableStatus?.let { actionable ->
|
||||
when (actionable.attachments[attachmentIndex].type) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
|
@ -150,27 +149,27 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context)
|
||||
context?.openLink(actionable.attachments[attachmentIndex].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
val actionableStatus = status.actionableStatus
|
||||
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
searchAdapter.peek(position)?.status?.let { status ->
|
||||
bottomSheetActivity?.viewAccount(status.account.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.expandedChange(it, expanded)
|
||||
}
|
||||
}
|
||||
|
@ -180,25 +179,25 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.collapsedChange(it, isCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
searchAdapter.item(position)?.let {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.voteInPoll(it, choices)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeItem(position: Int) {
|
||||
searchAdapter.item(position)?.let {
|
||||
private fun removeItem(position: Int) {
|
||||
searchAdapter.peek(position)?.let {
|
||||
viewModel.removeItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
searchAdapter.peek(position)?.let { status ->
|
||||
viewModel.reblog(status, reblog)
|
||||
}
|
||||
}
|
||||
|
@ -258,9 +257,6 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
val accountId = status.actionableStatus.account.id
|
||||
val accountUsername = status.actionableStatus.account.username
|
||||
val statusUrl = status.actionableStatus.url
|
||||
val accounts = viewModel.getAllAccountsOrderedByActive()
|
||||
var openAsTitle: String? = null
|
||||
|
||||
val loggedInAccountId = viewModel.activeAccount?.accountId
|
||||
|
||||
val popup = PopupMenu(view.context, view)
|
||||
|
@ -291,17 +287,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
val openAsItem = popup.menu.findItem(R.id.status_open_as)
|
||||
when (accounts.size) {
|
||||
0, 1 -> openAsItem.isVisible = false
|
||||
2 -> for (account in accounts) {
|
||||
if (account !== viewModel.activeAccount) {
|
||||
openAsTitle = String.format(getString(R.string.action_open_as), account.fullName)
|
||||
break
|
||||
}
|
||||
}
|
||||
else -> openAsTitle = String.format(getString(R.string.action_open_as), "…")
|
||||
val openAsText = bottomSheetActivity?.openAsText
|
||||
if (openAsText == null) {
|
||||
openAsItem.isVisible = false
|
||||
} else {
|
||||
openAsItem.title = openAsText
|
||||
}
|
||||
openAsItem.title = openAsTitle
|
||||
|
||||
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
|
||||
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
|
||||
|
@ -355,7 +346,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_mute_conversation -> {
|
||||
searchAdapter.item(position)?.let { foundStatus ->
|
||||
searchAdapter.peek(position)?.let { foundStatus ->
|
||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
|
@ -426,21 +417,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
dialogTitle, false,
|
||||
object : AccountSelectionListener {
|
||||
override fun onAccountSelected(account: AccountEntity) {
|
||||
openAsAccount(statusUrl, account)
|
||||
bottomSheetActivity?.openAsAccount(statusUrl, account)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun openAsAccount(statusUrl: String, account: AccountEntity) {
|
||||
viewModel.activeAccount = account
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl)
|
||||
startActivity(intent)
|
||||
(activity as BaseActivity).finishWithoutSlideOutAnimation()
|
||||
}
|
||||
|
||||
private fun downloadAllMedia(status: Status) {
|
||||
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
|
||||
for ((_, url) in status.attachments) {
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.AccountListActivity
|
|||
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.appstore.QuickReplyEvent
|
||||
|
@ -103,8 +104,6 @@ class TimelineFragment :
|
|||
|
||||
private var isSwipeToRefreshEnabled = true
|
||||
|
||||
private var eventRegistered = false
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
private var scrollListener: RecyclerView.OnScrollListener? = null
|
||||
private var hideFab = false
|
||||
|
@ -222,9 +221,11 @@ class TimelineFragment :
|
|||
|
||||
if (adapter.itemCount != itemCount) {
|
||||
binding.recyclerView.post {
|
||||
if (isSwipeToRefreshEnabled) {
|
||||
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
||||
} else binding.recyclerView.scrollToPosition(0)
|
||||
if (getView() != null) {
|
||||
if (isSwipeToRefreshEnabled) {
|
||||
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
||||
} else binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,36 +236,7 @@ class TimelineFragment :
|
|||
adapter.submitData(pagingData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSwipeRefreshLayout() {
|
||||
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binding.recyclerView.setAccessibilityDelegateCompat(
|
||||
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
|
||||
adapter.peek(pos)
|
||||
}
|
||||
)
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
||||
binding.recyclerView.addItemDecoration(divider)
|
||||
|
||||
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
||||
* guaranteed to be set until then. */
|
||||
if (actionButtonPresent()) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
hideFab = preferences.getBoolean("fabHide", false)
|
||||
|
@ -288,23 +260,43 @@ class TimelineFragment :
|
|||
}
|
||||
}
|
||||
|
||||
if (!eventRegistered) {
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event ->
|
||||
when (event) {
|
||||
is PreferenceChangedEvent -> {
|
||||
onPreferenceChanged(event.preferenceKey)
|
||||
}
|
||||
is StatusComposedEvent -> {
|
||||
val status = event.status
|
||||
handleStatusComposeEvent(status)
|
||||
}
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event ->
|
||||
when (event) {
|
||||
is PreferenceChangedEvent -> {
|
||||
onPreferenceChanged(event.preferenceKey)
|
||||
}
|
||||
is StatusComposedEvent -> {
|
||||
val status = event.status
|
||||
handleStatusComposeEvent(status)
|
||||
}
|
||||
}
|
||||
eventRegistered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSwipeRefreshLayout() {
|
||||
binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binding.recyclerView.setAccessibilityDelegateCompat(
|
||||
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
|
||||
adapter.peek(pos)
|
||||
}
|
||||
)
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
val divider = DividerItemDecoration(context, RecyclerView.VERTICAL)
|
||||
binding.recyclerView.addItemDecoration(divider)
|
||||
|
||||
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -449,7 +441,7 @@ class TimelineFragment :
|
|||
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
||||
if (enabled != oldMediaPreviewEnabled) {
|
||||
adapter.mediaPreviewEnabled = enabled
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -495,7 +487,7 @@ class TimelineFragment :
|
|||
talkBackWasEnabled = a11yManager?.isEnabled == true
|
||||
Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled")
|
||||
if (talkBackWasEnabled && !wasEnabled) {
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
startUpdateTimestamp()
|
||||
}
|
||||
|
@ -513,7 +505,7 @@ class TimelineFragment :
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
||||
.subscribe {
|
||||
adapter.notifyDataSetChanged()
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ class TimelinePagingAdapter(
|
|||
)
|
||||
}
|
||||
|
||||
init {
|
||||
stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_STATUS -> {
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
|||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
|
@ -41,6 +42,7 @@ data class Placeholder(
|
|||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
||||
|
||||
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||
return TimelineAccountEntity(
|
||||
|
@ -99,6 +101,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
|||
visibility = Status.Visibility.UNKNOWN,
|
||||
attachments = null,
|
||||
mentions = null,
|
||||
tags = null,
|
||||
application = null,
|
||||
reblogServerId = null,
|
||||
reblogAccountId = null,
|
||||
|
@ -138,6 +141,7 @@ fun Status.toEntity(
|
|||
visibility = actionableStatus.visibility,
|
||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
||||
tags = actionableStatus.tags.let(gson::toJson),
|
||||
application = actionableStatus.application.let(gson::toJson),
|
||||
reblogServerId = reblog?.id,
|
||||
reblogAccountId = reblog?.let { this.account.id },
|
||||
|
@ -157,6 +161,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
|
||||
val attachments: ArrayList<Attachment> = gson.fromJson(status.attachments, attachmentArrayListType) ?: arrayListOf()
|
||||
val mentions: List<Status.Mention> = gson.fromJson(status.mentions, mentionListType) ?: emptyList()
|
||||
val tags: List<HashTag>? = gson.fromJson(status.tags, tagListType)
|
||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||
|
@ -183,6 +188,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = application,
|
||||
pinned = false,
|
||||
muted = status.muted,
|
||||
|
@ -212,6 +218,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = ArrayList(),
|
||||
mentions = listOf(),
|
||||
tags = listOf(),
|
||||
application = null,
|
||||
pinned = status.pinned,
|
||||
muted = status.muted,
|
||||
|
@ -241,6 +248,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
|||
visibility = status.visibility,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
tags = tags,
|
||||
application = application,
|
||||
pinned = status.pinned,
|
||||
muted = status.muted,
|
||||
|
|
|
@ -33,7 +33,7 @@ import com.keylesspalace.tusky.util.dec
|
|||
import kotlinx.coroutines.rx3.await
|
||||
import retrofit2.HttpException
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class CachedTimelineRemoteMediator(
|
||||
accountManager: AccountManager,
|
||||
private val api: MastodonApi,
|
||||
|
@ -53,11 +53,19 @@ class CachedTimelineRemoteMediator(
|
|||
|
||||
try {
|
||||
var dbEmpty = false
|
||||
|
||||
val topPlaceholderId = if (loadType == LoadType.REFRESH) {
|
||||
timelineDao.getTopPlaceholderId(activeAccount.id)
|
||||
} else {
|
||||
null // don't execute the query if it is not needed
|
||||
}
|
||||
|
||||
if (!initialRefresh && loadType == LoadType.REFRESH) {
|
||||
val topId = timelineDao.getTopId(activeAccount.id)
|
||||
topId?.let { cachedTopId ->
|
||||
val statusResponse = api.homeTimeline(
|
||||
maxId = cachedTopId,
|
||||
sinceId = topPlaceholderId, // so already existing placeholders don't get accidentally overwritten
|
||||
limit = state.config.pageSize
|
||||
).await()
|
||||
|
||||
|
@ -74,7 +82,7 @@ class CachedTimelineRemoteMediator(
|
|||
|
||||
val statusResponse = when (loadType) {
|
||||
LoadType.REFRESH -> {
|
||||
api.homeTimeline(limit = state.config.pageSize).await()
|
||||
api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize).await()
|
||||
}
|
||||
LoadType.PREPEND -> {
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
|
|
|
@ -44,12 +44,15 @@ import com.keylesspalace.tusky.network.TimelineCases
|
|||
import com.keylesspalace.tusky.util.dec
|
||||
import com.keylesspalace.tusky.util.inc
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import net.accelf.yuito.streaming.StreamingManager
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
/**
|
||||
* TimelineViewModel that caches all statuses in a local database
|
||||
|
@ -66,11 +69,11 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
streamingManager: StreamingManager,
|
||||
) : TimelineViewModel(timelineCases, api, eventHub, accountManager, sharedPreferences, filterModel, streamingManager) {
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override val statuses = Pager(
|
||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
||||
pagingSourceFactory = { db.timelineDao().getStatusesForAccount(accountManager.activeAccount!!.id) }
|
||||
pagingSourceFactory = { db.timelineDao().getStatuses(accountManager.activeAccount!!.id) }
|
||||
).flow
|
||||
.map { pagingData ->
|
||||
pagingData.map { timelineStatus ->
|
||||
|
@ -84,6 +87,16 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
}
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
delay(5.toDuration(DurationUnit.SECONDS)) // delay so the db is not locked during initial ui refresh
|
||||
accountManager.activeAccount?.id?.let { accountId ->
|
||||
db.timelineDao().cleanup(accountId, MAX_STATUSES_IN_CACHE)
|
||||
db.timelineDao().cleanupAccounts(accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
||||
// handled by CacheUpdater
|
||||
}
|
||||
|
@ -131,7 +144,9 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
|
||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
||||
|
||||
val response = api.homeTimeline(maxId = placeholderId.inc(), limit = 20).await()
|
||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||
|
||||
val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE).await()
|
||||
|
||||
val statuses = response.body()
|
||||
if (!response.isSuccessful || statuses == null) {
|
||||
|
@ -165,13 +180,13 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
if (overlappedStatuses == 0) {
|
||||
if (overlappedStatuses == 0 && statuses.isNotEmpty()) {
|
||||
timelineDao.insertStatus(
|
||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
} catch (e: Exception) {
|
||||
loadMoreFailed(placeholderId, e)
|
||||
}
|
||||
}
|
||||
|
@ -230,10 +245,11 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
override fun fullReload() {
|
||||
viewModelScope.launch {
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
db.runInTransaction {
|
||||
db.timelineDao().removeAllForAccount(activeAccount.id)
|
||||
db.timelineDao().removeAllUsersForAccount(activeAccount.id)
|
||||
}
|
||||
db.timelineDao().removeAll(activeAccount.id)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_STATUSES_IN_CACHE = 1000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.toViewData
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import retrofit2.HttpException
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class NetworkTimelineRemoteMediator(
|
||||
private val accountManager: AccountManager,
|
||||
private val viewModel: NetworkTimelineViewModel
|
||||
|
@ -47,7 +47,11 @@ class NetworkTimelineRemoteMediator(
|
|||
}
|
||||
LoadType.APPEND -> {
|
||||
val maxId = viewModel.nextKey
|
||||
viewModel.fetchStatusesForKind(maxId, null, limit = state.config.pageSize)
|
||||
if (maxId != null) {
|
||||
viewModel.fetchStatusesForKind(maxId, null, limit = state.config.pageSize)
|
||||
} else {
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import com.keylesspalace.tusky.entity.Status
|
|||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import com.keylesspalace.tusky.util.inc
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -67,7 +67,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
|
||||
var nextKey: String? = null
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override val statuses = Pager(
|
||||
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
||||
pagingSourceFactory = {
|
||||
|
@ -121,7 +121,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
override fun removeAllByInstance(instance: String) {
|
||||
statusData.removeAll { vd ->
|
||||
val status = vd.asStatusOrNull()?.status ?: return@removeAll false
|
||||
LinkHelper.getDomain(status.account.url) == instance
|
||||
getDomain(status.account.url) == instance
|
||||
}
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import com.keylesspalace.tusky.settings.PrefKeys
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
|
|
@ -32,7 +32,7 @@ import java.io.File;
|
|||
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 28)
|
||||
}, version = 30)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
@ -457,4 +457,21 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
"ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_28_29 = new Migration(28, 29) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_tags` TEXT");
|
||||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `tags` TEXT");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_29_30 = new Migration(29, 30) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `charactersReservedPerUrl` INTEGER");
|
||||
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `minPollDuration` INTEGER");
|
||||
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` INTEGER");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
|||
import com.keylesspalace.tusky.createTabDataFromId
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -123,6 +124,16 @@ class Converters @Inject constructor (
|
|||
return gson.fromJson(mentionListJson, object : TypeToken<List<Status.Mention>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun tagListToJson(tagArray: List<HashTag>?): String? {
|
||||
return gson.toJson(tagArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTagArray(tagListJson: String?): List<HashTag>? {
|
||||
return gson.fromJson(tagListJson, object : TypeToken<List<HashTag>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun dateToLong(date: Date): Long {
|
||||
return date.time
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.net.toUri
|
|||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
@ -40,11 +41,16 @@ data class DraftEntity(
|
|||
val failedToSend: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* The alternate names are here because we accidentally published versions were DraftAttachment was minified
|
||||
* Tusky 15: uriString = e, description = f, type = g
|
||||
* Tusky 16 beta: uriString = i, description = j, type = k
|
||||
*/
|
||||
@Parcelize
|
||||
data class DraftAttachment(
|
||||
val uriString: String,
|
||||
val description: String?,
|
||||
val type: Type
|
||||
@SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String,
|
||||
@SerializedName(value = "description", alternate = ["f", "j"]) val description: String?,
|
||||
@SerializedName(value = "type", alternate = ["g", "k"]) val type: Type
|
||||
) : Parcelable {
|
||||
val uri: Uri
|
||||
get() = uriString.toUri()
|
||||
|
|
|
@ -28,5 +28,8 @@ data class InstanceEntity(
|
|||
val maximumTootCharacters: Int?,
|
||||
val maxPollOptions: Int?,
|
||||
val maxPollOptionLength: Int?,
|
||||
val minPollDuration: Int?,
|
||||
val maxPollDuration: Int?,
|
||||
val charactersReservedPerUrl: Int?,
|
||||
val version: String?
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ abstract class TimelineDao {
|
|||
SELECT s.serverId, s.url, s.timelineUserId,
|
||||
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
||||
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
||||
s.spoilerText, s.visibility, s.mentions, s.application, s.reblogServerId,s.reblogAccountId,
|
||||
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
||||
s.content, s.attachments, s.poll, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
||||
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
||||
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
||||
|
@ -51,7 +51,7 @@ LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND
|
|||
WHERE s.timelineUserId = :account
|
||||
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC"""
|
||||
)
|
||||
abstract fun getStatusesForAccount(account: Long): PagingSource<Int, TimelineStatusWithAccount>
|
||||
abstract fun getStatuses(account: Long): PagingSource<Int, TimelineStatusWithAccount>
|
||||
|
||||
@Query(
|
||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
|
||||
|
@ -86,11 +86,20 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId =
|
|||
)
|
||||
abstract fun removeAllByUser(accountId: Long, userId: String)
|
||||
|
||||
/**
|
||||
* Removes everything in the TimelineStatusEntity and TimelineAccountEntity tables for one user account
|
||||
* @param accountId id of the account for which to clean tables
|
||||
*/
|
||||
suspend fun removeAll(accountId: Long) {
|
||||
removeAllStatuses(accountId)
|
||||
removeAllAccounts(accountId)
|
||||
}
|
||||
|
||||
@Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
|
||||
abstract fun removeAllForAccount(accountId: Long)
|
||||
abstract suspend fun removeAllStatuses(accountId: Long)
|
||||
|
||||
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
|
||||
abstract fun removeAllUsersForAccount(accountId: Long)
|
||||
abstract suspend fun removeAllAccounts(accountId: Long)
|
||||
|
||||
@Query(
|
||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
|
||||
|
@ -98,8 +107,39 @@ AND serverId = :statusId"""
|
|||
)
|
||||
abstract fun delete(accountId: Long, statusId: String)
|
||||
|
||||
@Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""")
|
||||
abstract fun cleanup(olderThan: Long)
|
||||
/**
|
||||
* Cleans the TimelineStatusEntity and TimelineAccountEntity tables from old entries.
|
||||
* @param accountId id of the account for which to clean tables
|
||||
* @param limit how many statuses to keep
|
||||
*/
|
||||
suspend fun cleanup(accountId: Long, limit: Int) {
|
||||
cleanupStatuses(accountId, limit)
|
||||
cleanupAccounts(accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the TimelineStatusEntity table from old status entries.
|
||||
* @param accountId id of the account for which to clean statuses
|
||||
* @param limit how many statuses to keep
|
||||
*/
|
||||
@Query(
|
||||
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND serverId NOT IN
|
||||
(SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT :limit)
|
||||
"""
|
||||
)
|
||||
abstract suspend fun cleanupStatuses(accountId: Long, limit: Int)
|
||||
|
||||
/**
|
||||
* Cleans the TimelineAccountEntity table from accounts that are no longer referenced in the TimelineStatusEntity table
|
||||
* @param accountId id of the user account for which to clean timeline accounts
|
||||
*/
|
||||
@Query(
|
||||
"""DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId AND serverId NOT IN
|
||||
(SELECT authorServerId FROM TimelineStatusEntity WHERE timelineUserId = :accountId)
|
||||
AND serverId NOT IN
|
||||
(SELECT reblogAccountId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND reblogAccountId IS NOT NULL)"""
|
||||
)
|
||||
abstract suspend fun cleanupAccounts(accountId: Long)
|
||||
|
||||
@Query(
|
||||
"""UPDATE TimelineStatusEntity SET poll = :poll
|
||||
|
@ -142,4 +182,10 @@ AND timelineUserId = :accountId
|
|||
|
||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||
abstract suspend fun getTopId(accountId: Long): String?
|
||||
|
||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
|
||||
|
||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ data class TimelineStatusEntity(
|
|||
val visibility: Status.Visibility,
|
||||
val attachments: String?,
|
||||
val mentions: String?,
|
||||
val tags: String?,
|
||||
val application: String?,
|
||||
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
||||
val reblogAccountId: String?,
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.AboutActivity
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.EditProfileActivity
|
||||
|
@ -25,13 +24,12 @@ import com.keylesspalace.tusky.LicenseActivity
|
|||
import com.keylesspalace.tusky.ListsActivity
|
||||
import com.keylesspalace.tusky.LoginActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.ModalTimelineActivity
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.ViewThreadActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||
|
@ -72,12 +70,6 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesAccountListActivity(): AccountListActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesModalTimelineActivity(): ModalTimelineActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesViewTagActivity(): ViewTagActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
||||
|
||||
|
|
|
@ -34,8 +34,7 @@ import javax.inject.Singleton
|
|||
ActivitiesModule::class,
|
||||
ServicesModule::class,
|
||||
BroadcastReceiverModule::class,
|
||||
ViewModelModule::class,
|
||||
MediaUploaderModule::class
|
||||
ViewModelModule::class
|
||||
]
|
||||
)
|
||||
interface AppComponent {
|
||||
|
|
|
@ -19,19 +19,11 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.Room
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.EventHubImpl
|
||||
import com.keylesspalace.tusky.components.notifications.Notifier
|
||||
import com.keylesspalace.tusky.components.notifications.SystemNotifier
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.network.TimelineCasesImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
@ -54,28 +46,11 @@ class AppModule {
|
|||
return PreferenceManager.getDefaultSharedPreferences(app)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesBroadcastManager(app: Application): LocalBroadcastManager {
|
||||
return LocalBroadcastManager.getInstance(app)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesConnectivityManager(appContext: Context): ConnectivityManager {
|
||||
return appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesTimelineUseCases(
|
||||
api: MastodonApi,
|
||||
eventHub: EventHub
|
||||
): TimelineCases {
|
||||
return TimelineCasesImpl(api, eventHub)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesEventHub(): EventHub = EventHubImpl
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(appContext: Context, converters: Converters): AppDatabase {
|
||||
|
@ -92,12 +67,9 @@ class AppModule {
|
|||
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
||||
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
||||
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28
|
||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||
AppDatabase.MIGRATION_29_30
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notifier(context: Context): Notifier = SystemNotifier(context)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.AccountsInListFragment
|
||||
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
||||
import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
|
||||
|
@ -30,7 +31,6 @@ import com.keylesspalace.tusky.components.search.fragments.SearchNotestockFragme
|
|||
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.fragment.AccountListFragment
|
||||
import com.keylesspalace.tusky.fragment.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
||||
import com.keylesspalace.tusky.fragment.ViewThreadFragment
|
||||
import dagger.Module
|
||||
|
@ -57,9 +57,6 @@ abstract class FragmentBuildersModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun notificationsFragment(): NotificationsFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun searchFragment(): SearchStatusesFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun notificationPreferencesFragment(): NotificationPreferencesFragment
|
||||
|
||||
|
@ -67,7 +64,7 @@ abstract class FragmentBuildersModule {
|
|||
abstract fun accountPreferencesFragment(): AccountPreferencesFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun directMessagesPreferencesFragment(): ConversationsFragment
|
||||
abstract fun conversationsFragment(): ConversationsFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun accountInListsFragment(): AccountsInListFragment
|
||||
|
@ -84,6 +81,9 @@ abstract class FragmentBuildersModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun instanceListFragment(): InstanceListFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun searchStatusesFragment(): SearchStatusesFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun searchAccountFragment(): SearchAccountsFragment
|
||||
|
||||
|
|
|
@ -15,25 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.service.SendTootService
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.ServiceClientImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@Module
|
||||
abstract class ServicesModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSendTootService(): SendTootService
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
@Provides
|
||||
@JvmStatic
|
||||
fun providesServiceClient(context: Context): ServiceClient {
|
||||
return ServiceClientImpl(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.components.account.AccountViewModel
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||
|
@ -13,7 +14,6 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
|
|||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class HashTag(val name: String)
|
||||
data class HashTag(val name: String, val url: String)
|
||||
|
|
|
@ -30,7 +30,8 @@ data class Instance(
|
|||
@SerializedName("contact_account") val contactAccount: Account,
|
||||
@SerializedName("max_toot_chars") val maxTootChars: Int?,
|
||||
@SerializedName("max_bio_chars") val maxBioChars: Int?,
|
||||
@SerializedName("poll_limits") val pollLimits: PollLimits?
|
||||
@SerializedName("poll_limits") val pollConfiguration: PollConfiguration?,
|
||||
val configuration: InstanceConfiguration?,
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
return uri.hashCode()
|
||||
|
@ -45,7 +46,31 @@ data class Instance(
|
|||
}
|
||||
}
|
||||
|
||||
data class PollLimits(
|
||||
data class PollConfiguration(
|
||||
@SerializedName("max_options") val maxOptions: Int?,
|
||||
@SerializedName("max_option_chars") val maxOptionChars: Int?
|
||||
@SerializedName("max_option_chars") val maxOptionChars: Int?,
|
||||
@SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?,
|
||||
@SerializedName("min_expiration") val minExpiration: Int?,
|
||||
@SerializedName("max_expiration") val maxExpiration: Int?,
|
||||
)
|
||||
|
||||
data class InstanceConfiguration(
|
||||
val statuses: StatusConfiguration?,
|
||||
@SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?,
|
||||
val polls: PollConfiguration?,
|
||||
)
|
||||
|
||||
data class StatusConfiguration(
|
||||
@SerializedName("max_characters") val maxCharacters: Int?,
|
||||
@SerializedName("max_media_attachments") val maxMediaAttachments: Int?,
|
||||
@SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?,
|
||||
)
|
||||
|
||||
data class MediaAttachmentConfiguration(
|
||||
@SerializedName("supported_mime_types") val supportedMimeTypes: List<String>?,
|
||||
@SerializedName("image_size_limit") val imageSizeLimit: Int?,
|
||||
@SerializedName("image_matrix_limit") val imageMatrixLimit: Int?,
|
||||
@SerializedName("video_size_limit") val videoSizeLimit: Int?,
|
||||
@SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?,
|
||||
@SerializedName("video_matrix_limit") val videoMatrixLimit: Int?,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
/**
|
||||
* The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/
|
||||
* We are only interested in the id, so other attributes are omitted
|
||||
*/
|
||||
data class MediaUploadResult(
|
||||
val id: String
|
||||
)
|
|
@ -15,7 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.json.GuardedBooleanAdapter
|
||||
|
||||
data class Relationship(
|
||||
val id: String,
|
||||
|
@ -26,7 +28,11 @@ data class Relationship(
|
|||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
val subscribing: Boolean? = null, // Pleroma extension
|
||||
/* Pleroma extension, same as 'notifying' on Mastodon.
|
||||
* Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object,
|
||||
* so we use the custom GuardedBooleanAdapter to ignore the field if it is not a boolean.
|
||||
*/
|
||||
@JsonAdapter(GuardedBooleanAdapter::class) val subscribing: Boolean? = null,
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
val note: String?, // nullable for backward compatibility / feature detection
|
||||
val notifying: Boolean? // since 3.3.0rc
|
||||
|
|
|
@ -42,6 +42,7 @@ data class Status(
|
|||
val visibility: Visibility,
|
||||
@SerializedName("media_attachments", alternate = ["attachment"]) var attachments: ArrayList<Attachment>,
|
||||
val mentions: List<Mention>,
|
||||
val tags: List<HashTag>?,
|
||||
val application: Application?,
|
||||
val pinned: Boolean?,
|
||||
val muted: Boolean?,
|
||||
|
@ -59,10 +60,11 @@ data class Status(
|
|||
val isNotestock: Boolean
|
||||
get() = !account.notestockUsername.isNullOrEmpty()
|
||||
|
||||
/** Helper for Java */
|
||||
/** Helpers for Java */
|
||||
fun copyWithFavourited(favourited: Boolean): Status = copy(favourited = favourited)
|
||||
fun copyWithReblogged(reblogged: Boolean): Status = copy(reblogged = reblogged)
|
||||
fun copyWithBookmarked(bookmarked: Boolean): Status = copy(bookmarked = bookmarked)
|
||||
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
||||
|
||||
enum class Visibility(val num: Int) {
|
||||
|
@ -156,18 +158,6 @@ data class Status(
|
|||
return builder.toString()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val status = other as Status?
|
||||
return id == status?.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val url: String,
|
||||
|
|
|
@ -29,7 +29,6 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.AccountListActivity.Type
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
|
@ -39,6 +38,7 @@ import com.keylesspalace.tusky.adapter.FollowAdapter
|
|||
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter
|
||||
import com.keylesspalace.tusky.adapter.FollowRequestsHeaderAdapter
|
||||
import com.keylesspalace.tusky.adapter.MutesAdapter
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
|
|
@ -188,9 +188,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
Notification notification = input.asRight()
|
||||
.rewriteToStatusTypeIfNeeded(accountManager.getActiveAccount().getAccountId());
|
||||
|
||||
boolean sensitiveStatus = notification.getStatus() != null && notification.getStatus().getActionableStatus().getSensitive();
|
||||
|
||||
return ViewDataUtils.notificationToViewData(
|
||||
notification,
|
||||
alwaysShowSensitiveMedia,
|
||||
alwaysShowSensitiveMedia || !sensitiveStatus,
|
||||
alwaysOpenSpoiler,
|
||||
true
|
||||
);
|
||||
|
@ -416,10 +418,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void setReblogForStatus(String statusId, boolean reblog) {
|
||||
updateStatus(statusId, (s) -> {
|
||||
s.setReblogged(reblog);
|
||||
return s;
|
||||
});
|
||||
updateStatus(statusId, (s) -> s.copyWithReblogged(reblog));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -438,10 +437,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void setFavouriteForStatus(String statusId, boolean favourite) {
|
||||
updateStatus(statusId, (s) -> {
|
||||
s.setFavourited(favourite);
|
||||
return s;
|
||||
});
|
||||
updateStatus(statusId, (s) -> s.copyWithFavourited(favourite));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -460,10 +456,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void setBookmarkForStatus(String statusId, boolean bookmark) {
|
||||
updateStatus(statusId, (s) -> {
|
||||
s.setBookmarked(bookmark);
|
||||
return s;
|
||||
});
|
||||
updateStatus(statusId, (s) -> s.copyWithBookmarked(bookmark));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -528,10 +521,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void setPinForStatus(String statusId, boolean pinned) {
|
||||
updateStatus(statusId, status -> {
|
||||
status.copyWithPinned(pinned);
|
||||
return status;
|
||||
});
|
||||
updateStatus(statusId, status -> status.copyWithPinned(pinned));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -41,11 +41,10 @@ import androidx.lifecycle.Lifecycle;
|
|||
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.StatusListActivity;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.ViewTagActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||
|
@ -189,8 +188,6 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
final String accountId = status.getActionableStatus().getAccount().getId();
|
||||
final String accountUsername = status.getActionableStatus().getAccount().getUsername();
|
||||
final String statusUrl = status.getActionableStatus().getUrl();
|
||||
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
||||
String openAsTitle = null;
|
||||
|
||||
String loggedInAccountId = null;
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
|
@ -228,24 +225,12 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
|
||||
Menu menu = popup.getMenu();
|
||||
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
||||
switch (accounts.size()) {
|
||||
case 0:
|
||||
case 1:
|
||||
openAsItem.setVisible(false);
|
||||
break;
|
||||
case 2:
|
||||
for (AccountEntity account : accounts) {
|
||||
if (account != activeAccount) {
|
||||
openAsTitle = String.format(getString(R.string.action_open_as), account.getFullName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
openAsTitle = String.format(getString(R.string.action_open_as), "…");
|
||||
break;
|
||||
String openAsText = ((BaseActivity)getActivity()).getOpenAsText();
|
||||
if (openAsText == null) {
|
||||
openAsItem.setVisible(false);
|
||||
} else {
|
||||
openAsItem.setTitle(openAsText);
|
||||
}
|
||||
openAsItem.setTitle(openAsTitle);
|
||||
|
||||
MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation);
|
||||
boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions());
|
||||
|
@ -405,15 +390,14 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
}
|
||||
default:
|
||||
case UNKNOWN: {
|
||||
LinkHelper.openLink(active.getAttachment().getUrl(), getContext());
|
||||
LinkHelper.openLink(requireContext(), active.getAttachment().getUrl());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void viewTag(String tag) {
|
||||
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
||||
intent.putExtra("hashtag", tag);
|
||||
Intent intent = StatusListActivity.newHashtagIntent(requireContext(), tag);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -483,18 +467,9 @@ public abstract class SFragment extends Fragment implements Injectable {
|
|||
.show();
|
||||
}
|
||||
|
||||
private void openAsAccount(String statusUrl, AccountEntity account) {
|
||||
accountManager.setActiveAccount(account);
|
||||
Intent intent = new Intent(getContext(), MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl);
|
||||
startActivity(intent);
|
||||
((BaseActivity) getActivity()).finishWithoutSlideOutAnimation();
|
||||
}
|
||||
|
||||
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
||||
activity.showAccountChooserDialog(dialogTitle, false, account -> activity.openAsAccount(statusUrl, account));
|
||||
}
|
||||
|
||||
private void downloadAllMedia(Status status) {
|
||||
|
|
|
@ -61,6 +61,7 @@ import com.keylesspalace.tusky.network.FilterModel;
|
|||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.settings.PrefKeys;
|
||||
import com.keylesspalace.tusky.util.CardViewMode;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
|
@ -107,10 +108,10 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
private final PairedList<Status, StatusViewData.Concrete> statuses =
|
||||
new PairedList<>(new Function<Status, StatusViewData.Concrete>() {
|
||||
@Override
|
||||
public StatusViewData.Concrete apply(Status input) {
|
||||
public StatusViewData.Concrete apply(Status status) {
|
||||
return ViewDataUtils.statusToViewData(
|
||||
input,
|
||||
alwaysShowSensitiveMedia,
|
||||
status,
|
||||
alwaysShowSensitiveMedia || !status.getActionableStatus().getSensitive(),
|
||||
alwaysOpenSpoiler,
|
||||
true
|
||||
);
|
||||
|
@ -327,6 +328,22 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
super.viewThread(status.getActionableId(), status.getActionableStatus().getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewUrl(String url, String text) {
|
||||
Status status = null;
|
||||
if (!statuses.isEmpty()) {
|
||||
status = statuses.get(statusIndex);
|
||||
}
|
||||
if (status != null && status.getUrl().equals(url)) {
|
||||
// already viewing the status with this url
|
||||
// probably just a preview federated and the user is clicking again to view more -> open the browser
|
||||
// this can happen with some friendica statuses
|
||||
LinkHelper.openLink(requireContext(), url);
|
||||
return;
|
||||
}
|
||||
super.onViewUrl(url, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
// there should be no reblogs in the thread but let's implement it to be sure
|
||||
|
@ -490,7 +507,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
private int setStatus(Status status) {
|
||||
if (statuses.size() > 0
|
||||
&& statusIndex < statuses.size()
|
||||
&& statuses.get(statusIndex).equals(status)) {
|
||||
&& statuses.get(statusIndex).getId().equals(status.getId())) {
|
||||
// Do not add this status on refresh, it's already in there.
|
||||
statuses.set(statusIndex, status);
|
||||
return statusIndex;
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.interfaces;
|
||||
package com.keylesspalace.tusky.interfaces
|
||||
|
||||
public interface LinkListener {
|
||||
void onViewTag(String tag);
|
||||
void onViewAccount(String id);
|
||||
void onViewUrl(String url, String text);
|
||||
interface LinkListener {
|
||||
fun onViewTag(tag: String)
|
||||
fun onViewAccount(id: String)
|
||||
fun onViewUrl(url: String, text: String)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Tusky Contributors
|
||||
/* Copyright 2022 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
@ -13,18 +13,21 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.di
|
||||
package com.keylesspalace.tusky.json
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.components.compose.MediaUploader
|
||||
import com.keylesspalace.tusky.components.compose.MediaUploaderImpl
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
@Module
|
||||
class MediaUploaderModule {
|
||||
@Provides
|
||||
fun providesMediaUploder(context: Context, mastodonApi: MastodonApi): MediaUploader =
|
||||
MediaUploaderImpl(context, mastodonApi)
|
||||
class GuardedBooleanAdapter : JsonDeserializer<Boolean?> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? {
|
||||
return if (json.isJsonObject) {
|
||||
null
|
||||
} else {
|
||||
json.asBoolean
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import com.keylesspalace.tusky.entity.IdentityProof
|
|||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.entity.Marker
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.entity.MediaUploadResult
|
||||
import com.keylesspalace.tusky.entity.NewStatus
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
|
@ -142,11 +143,11 @@ interface MastodonApi {
|
|||
fun clearNotifications(): Single<ResponseBody>
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/media")
|
||||
@POST("api/v2/media")
|
||||
fun uploadMedia(
|
||||
@Part file: MultipartBody.Part,
|
||||
@Part description: MultipartBody.Part? = null
|
||||
): Single<Attachment>
|
||||
): Single<MediaUploadResult>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/media/{mediaId}")
|
||||
|
|
|
@ -32,27 +32,16 @@ import com.keylesspalace.tusky.entity.Status
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/24/18.
|
||||
*/
|
||||
|
||||
interface TimelineCases {
|
||||
fun reblog(statusId: String, reblog: Boolean): Single<Status>
|
||||
fun favourite(statusId: String, favourite: Boolean): Single<Status>
|
||||
fun bookmark(statusId: String, bookmark: Boolean): Single<Status>
|
||||
fun mute(statusId: String, notifications: Boolean, duration: Int?)
|
||||
fun block(statusId: String)
|
||||
fun delete(statusId: String): Single<DeletedStatus>
|
||||
fun pin(statusId: String, pin: Boolean): Single<Status>
|
||||
fun voteInPoll(statusId: String, pollId: String, choices: List<Int>): Single<Poll>
|
||||
fun muteConversation(statusId: String, mute: Boolean): Single<Status>
|
||||
}
|
||||
|
||||
class TimelineCasesImpl(
|
||||
class TimelineCases @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
) : TimelineCases {
|
||||
) {
|
||||
|
||||
/**
|
||||
* Unused yet but can be use for cancellation later. It's always a good idea to save
|
||||
|
@ -60,7 +49,7 @@ class TimelineCasesImpl(
|
|||
*/
|
||||
private val cancelDisposable = CompositeDisposable()
|
||||
|
||||
override fun reblog(statusId: String, reblog: Boolean): Single<Status> {
|
||||
fun reblog(statusId: String, reblog: Boolean): Single<Status> {
|
||||
val call = if (reblog) {
|
||||
mastodonApi.reblogStatus(statusId)
|
||||
} else {
|
||||
|
@ -71,7 +60,7 @@ class TimelineCasesImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun favourite(statusId: String, favourite: Boolean): Single<Status> {
|
||||
fun favourite(statusId: String, favourite: Boolean): Single<Status> {
|
||||
val call = if (favourite) {
|
||||
mastodonApi.favouriteStatus(statusId)
|
||||
} else {
|
||||
|
@ -82,7 +71,7 @@ class TimelineCasesImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun bookmark(statusId: String, bookmark: Boolean): Single<Status> {
|
||||
fun bookmark(statusId: String, bookmark: Boolean): Single<Status> {
|
||||
val call = if (bookmark) {
|
||||
mastodonApi.bookmarkStatus(statusId)
|
||||
} else {
|
||||
|
@ -93,7 +82,7 @@ class TimelineCasesImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun muteConversation(statusId: String, mute: Boolean): Single<Status> {
|
||||
fun muteConversation(statusId: String, mute: Boolean): Single<Status> {
|
||||
val call = if (mute) {
|
||||
mastodonApi.muteConversation(statusId)
|
||||
} else {
|
||||
|
@ -104,7 +93,7 @@ class TimelineCasesImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun mute(statusId: String, notifications: Boolean, duration: Int?) {
|
||||
fun mute(statusId: String, notifications: Boolean, duration: Int?) {
|
||||
mastodonApi.muteAccount(statusId, notifications, duration)
|
||||
.subscribe(
|
||||
{
|
||||
|
@ -117,7 +106,7 @@ class TimelineCasesImpl(
|
|||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun block(statusId: String) {
|
||||
fun block(statusId: String) {
|
||||
mastodonApi.blockAccount(statusId)
|
||||
.subscribe(
|
||||
{
|
||||
|
@ -130,14 +119,14 @@ class TimelineCasesImpl(
|
|||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun delete(statusId: String): Single<DeletedStatus> {
|
||||
fun delete(statusId: String): Single<DeletedStatus> {
|
||||
return mastodonApi.deleteStatus(statusId)
|
||||
.doAfterSuccess {
|
||||
eventHub.dispatch(StatusDeletedEvent(statusId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pin(statusId: String, pin: Boolean): Single<Status> {
|
||||
fun pin(statusId: String, pin: Boolean): Single<Status> {
|
||||
// Replace with extension method if we use RxKotlin
|
||||
return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId))
|
||||
.doAfterSuccess {
|
||||
|
@ -145,7 +134,7 @@ class TimelineCasesImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun voteInPoll(statusId: String, pollId: String, choices: List<Int>): Single<Poll> {
|
||||
fun voteInPoll(statusId: String, pollId: String, choices: List<Int>): Single<Poll> {
|
||||
if (choices.isEmpty()) {
|
||||
return Single.error(IllegalStateException())
|
||||
}
|
||||
|
|
|
@ -16,19 +16,12 @@
|
|||
package com.keylesspalace.tusky.service
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ServiceClient {
|
||||
fun sendToot(tootToSend: TootToSend)
|
||||
}
|
||||
|
||||
class ServiceClientImpl(private val context: Context) : ServiceClient {
|
||||
override fun sendToot(tootToSend: TootToSend) {
|
||||
class ServiceClient @Inject constructor(private val context: Context) {
|
||||
fun sendToot(tootToSend: TootToSend) {
|
||||
val intent = SendTootService.sendTootIntent(context, tootToSend)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import java.util.regex.Pattern
|
|||
|
||||
/**
|
||||
* replaces emoji shortcodes in a text with EmojiSpans
|
||||
* @param text the text containing custom emojis
|
||||
* @receiver the text containing custom emojis
|
||||
* @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances)
|
||||
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
|
||||
* @return the text with the shortcodes replaced by EmojiSpans
|
||||
|
@ -44,7 +44,7 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean): Ch
|
|||
|
||||
val builder = SpannableStringBuilder.valueOf(this)
|
||||
|
||||
emojis.forEach { (shortcode, url) ->
|
||||
emojis.forEach { (shortcode, url, staticUrl) ->
|
||||
val matcher = Pattern.compile(":$shortcode:", Pattern.LITERAL)
|
||||
.matcher(this)
|
||||
|
||||
|
@ -54,7 +54,7 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean): Ch
|
|||
builder.setSpan(span, matcher.start(), matcher.end(), 0)
|
||||
Glide.with(view)
|
||||
.asDrawable()
|
||||
.load(url)
|
||||
.load(if (animate) { url } else { staticUrl })
|
||||
.into(span.getTarget(animate))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,256 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
public class LinkHelper {
|
||||
public static String getDomain(String urlString) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
} catch (URISyntaxException e) {
|
||||
return "";
|
||||
}
|
||||
String host = uri.getHost();
|
||||
if(host == null) {
|
||||
return "";
|
||||
} else if (host.startsWith("www.")) {
|
||||
return host.substring(4);
|
||||
} else {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating
|
||||
* them with callbacks to notify when they're clicked.
|
||||
*
|
||||
* @param view the returned text will be put in
|
||||
* @param content containing text with mentions, links, or hashtags
|
||||
* @param mentions any '@' mentions which are known to be in the content
|
||||
* @param listener to notify about particular spans that are clicked
|
||||
*/
|
||||
public static void setClickableText(TextView view, CharSequence content,
|
||||
@Nullable List<Status.Mention> mentions, final LinkListener listener) {
|
||||
SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content);
|
||||
URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
int start = builder.getSpanStart(span);
|
||||
int end = builder.getSpanEnd(span);
|
||||
int flags = builder.getSpanFlags(span);
|
||||
CharSequence text = builder.subSequence(start, end);
|
||||
ClickableSpan customSpan = null;
|
||||
|
||||
if (text.charAt(0) == '#') {
|
||||
final String tag = text.subSequence(1, text.length()).toString();
|
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) { listener.onViewTag(tag); }
|
||||
};
|
||||
} else if (text.charAt(0) == '@' && mentions != null && mentions.size() > 0) {
|
||||
String accountUsername = text.subSequence(1, text.length()).toString();
|
||||
/* There may be multiple matches for users on different instances with the same
|
||||
* username. If a match has the same domain we know it's for sure the same, but if
|
||||
* that can't be found then just go with whichever one matched last. */
|
||||
String id = null;
|
||||
for (Status.Mention mention : mentions) {
|
||||
if (mention.getLocalUsername().equalsIgnoreCase(accountUsername)) {
|
||||
id = mention.getId();
|
||||
if (mention.getUrl().contains(getDomain(span.getURL()))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
final String accountId = id;
|
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (customSpan == null) {
|
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
listener.onViewUrl(getURL(), text.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(customSpan, start, end, flags);
|
||||
|
||||
/* Add zero-width space after links in end of line to fix its too large hitbox.
|
||||
* See also : https://github.com/tuskyapp/Tusky/issues/846
|
||||
* https://github.com/tuskyapp/Tusky/pull/916 */
|
||||
if (end >= builder.length() ||
|
||||
builder.subSequence(end, end + 1).toString().equals("\n")){
|
||||
builder.insert(end, "\u200B");
|
||||
}
|
||||
}
|
||||
|
||||
view.setText(builder);
|
||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Put mentions in a piece of text and makes them clickable, associating them with callbacks to
|
||||
* notify when they're clicked.
|
||||
*
|
||||
* @param view the returned text will be put in
|
||||
* @param mentions any '@' mentions which are known to be in the content
|
||||
* @param listener to notify about particular spans that are clicked
|
||||
*/
|
||||
public static void setClickableMentions(
|
||||
TextView view, @Nullable List<Status.Mention> mentions, final LinkListener listener) {
|
||||
if (mentions == null || mentions.size() == 0) {
|
||||
view.setText(null);
|
||||
return;
|
||||
}
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
int flags;
|
||||
boolean firstMention = true;
|
||||
for (Status.Mention mention : mentions) {
|
||||
String accountUsername = mention.getLocalUsername();
|
||||
final String accountId = mention.getId();
|
||||
ClickableSpan customSpan = new NoUnderlineURLSpan(mention.getUrl()) {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); }
|
||||
};
|
||||
|
||||
end += 1 + accountUsername.length(); // length of @ + username
|
||||
flags = builder.getSpanFlags(customSpan);
|
||||
if (firstMention) {
|
||||
firstMention = false;
|
||||
} else {
|
||||
builder.append(" ");
|
||||
start += 1;
|
||||
end += 1;
|
||||
}
|
||||
builder.append("@");
|
||||
builder.append(accountUsername);
|
||||
builder.setSpan(customSpan, start, end, flags);
|
||||
builder.append("\u200B"); // same reasonning than in setClickableText
|
||||
end += 1; // shift position to take the previous character into account
|
||||
start = end;
|
||||
}
|
||||
view.setText(builder);
|
||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public static CharSequence createClickableText(String text, String link) {
|
||||
URLSpan span = new NoUnderlineURLSpan(link);
|
||||
|
||||
SpannableStringBuilder clickableText = new SpannableStringBuilder(text);
|
||||
clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
return clickableText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
||||
*
|
||||
* @param url a string containing the url to open
|
||||
* @param context context
|
||||
*/
|
||||
public static void openLink(String url, Context context) {
|
||||
Uri uri = Uri.parse(url).normalizeScheme();
|
||||
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("customTabs", false);
|
||||
if (useCustomTabs) {
|
||||
openLinkInCustomTab(uri, context);
|
||||
} else {
|
||||
openLinkInBrowser(uri, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* opens a link in the browser via Intent.ACTION_VIEW
|
||||
*
|
||||
* @param uri the uri to open
|
||||
* @param context context
|
||||
*/
|
||||
public static void openLinkInBrowser(Uri uri, Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w("LinkHelper", "Actvity was not found for intent, " + intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tries to open a link in a custom tab
|
||||
* falls back to browser if not possible
|
||||
*
|
||||
* @param uri the uri to open
|
||||
* @param context context
|
||||
*/
|
||||
public static void openLinkInCustomTab(Uri uri, Context context) {
|
||||
int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface);
|
||||
int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor);
|
||||
int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor);
|
||||
|
||||
CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setNavigationBarColor(navigationbarColor)
|
||||
.setNavigationBarDividerColor(navigationbarDividerColor)
|
||||
.build();
|
||||
|
||||
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(colorSchemeParams)
|
||||
.setShowTitle(true)
|
||||
.build();
|
||||
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, uri);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent);
|
||||
openLinkInBrowser(uri, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
@file:JvmName("LinkHelper")
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status.Mention
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
|
||||
fun getDomain(urlString: String?): String {
|
||||
val host = urlString?.toUri()?.host
|
||||
return when {
|
||||
host == null -> ""
|
||||
host.startsWith("www.") -> host.substring(4)
|
||||
else -> host
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating
|
||||
* them with callbacks to notify when they're clicked.
|
||||
*
|
||||
* @param view the returned text will be put in
|
||||
* @param content containing text with mentions, links, or hashtags
|
||||
* @param mentions any '@' mentions which are known to be in the content
|
||||
* @param listener to notify about particular spans that are clicked
|
||||
*/
|
||||
fun setClickableText(view: TextView, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener) {
|
||||
view.text = SpannableStringBuilder.valueOf(content).apply {
|
||||
getSpans(0, content.length, URLSpan::class.java).forEach {
|
||||
setClickableText(it, this, mentions, tags, listener)
|
||||
}
|
||||
}
|
||||
view.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setClickableText(
|
||||
span: URLSpan,
|
||||
builder: SpannableStringBuilder,
|
||||
mentions: List<Mention>,
|
||||
tags: List<HashTag>?,
|
||||
listener: LinkListener
|
||||
) = builder.apply {
|
||||
val start = getSpanStart(span)
|
||||
val end = getSpanEnd(span)
|
||||
val flags = getSpanFlags(span)
|
||||
val text = subSequence(start, end)
|
||||
|
||||
val customSpan = when (text[0]) {
|
||||
'#' -> getCustomSpanForTag(text, tags, span, listener)
|
||||
'@' -> getCustomSpanForMention(mentions, span, listener)
|
||||
else -> null
|
||||
} ?: object : NoUnderlineURLSpan(span.url) {
|
||||
override fun onClick(view: View) = listener.onViewUrl(url, text.toString())
|
||||
}
|
||||
|
||||
removeSpan(span)
|
||||
setSpan(customSpan, start, end, flags)
|
||||
|
||||
/* Add zero-width space after links in end of line to fix its too large hitbox.
|
||||
* See also : https://github.com/tuskyapp/Tusky/issues/846
|
||||
* https://github.com/tuskyapp/Tusky/pull/916 */
|
||||
if (end >= length || subSequence(end, end + 1).toString() == "\n") {
|
||||
insert(end, "\u200B")
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getTagName(text: CharSequence, tags: List<HashTag>?): String? {
|
||||
val scrapedName = text.subSequence(1, text.length).toString()
|
||||
return when (tags) {
|
||||
null -> scrapedName
|
||||
else -> tags.firstOrNull { it.name.equals(scrapedName, true) }?.name
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomSpanForTag(text: CharSequence, tags: List<HashTag>?, span: URLSpan, listener: LinkListener): ClickableSpan? {
|
||||
return getTagName(text, tags)?.let {
|
||||
object : NoUnderlineURLSpan(span.url) {
|
||||
override fun onClick(view: View) = listener.onViewTag(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomSpanForMention(mentions: List<Mention>, span: URLSpan, listener: LinkListener): ClickableSpan? {
|
||||
// https://github.com/tuskyapp/Tusky/pull/2339
|
||||
return mentions.firstOrNull { it.url == span.url }?.let {
|
||||
getCustomSpanForMentionUrl(span.url, it.id, listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomSpanForMentionUrl(url: String, mentionId: String, listener: LinkListener): ClickableSpan {
|
||||
return object : NoUnderlineURLSpan(url) {
|
||||
override fun onClick(view: View) = listener.onViewAccount(mentionId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put mentions in a piece of text and makes them clickable, associating them with callbacks to
|
||||
* notify when they're clicked.
|
||||
*
|
||||
* @param view the returned text will be put in
|
||||
* @param mentions any '@' mentions which are known to be in the content
|
||||
* @param listener to notify about particular spans that are clicked
|
||||
*/
|
||||
fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: LinkListener) {
|
||||
if (mentions?.isEmpty() != false) {
|
||||
view.text = null
|
||||
return
|
||||
}
|
||||
|
||||
view.text = SpannableStringBuilder().apply {
|
||||
var start = 0
|
||||
var end = 0
|
||||
var flags: Int
|
||||
var firstMention = true
|
||||
|
||||
for (mention in mentions) {
|
||||
val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener)
|
||||
end += 1 + mention.username.length // length of @ + username
|
||||
flags = getSpanFlags(customSpan)
|
||||
if (firstMention) {
|
||||
firstMention = false
|
||||
} else {
|
||||
append(" ")
|
||||
start += 1
|
||||
end += 1
|
||||
}
|
||||
|
||||
append("@")
|
||||
append(mention.username)
|
||||
setSpan(customSpan, start, end, flags)
|
||||
append("\u200B") // same reasoning as in setClickableText
|
||||
end += 1 // shift position to take the previous character into account
|
||||
start = end
|
||||
}
|
||||
}
|
||||
view.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
fun createClickableText(text: String, link: String): CharSequence {
|
||||
return SpannableStringBuilder(text).apply {
|
||||
setSpan(NoUnderlineURLSpan(link), 0, text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
||||
*
|
||||
* @receiver the Context to open the link from
|
||||
* @param url a string containing the url to open
|
||||
*/
|
||||
fun Context.openLink(url: String) {
|
||||
val uri = url.toUri().normalizeScheme()
|
||||
val useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("customTabs", false)
|
||||
|
||||
if (useCustomTabs) {
|
||||
openLinkInCustomTab(uri, this)
|
||||
} else {
|
||||
openLinkInBrowser(uri, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* opens a link in the browser via Intent.ACTION_VIEW
|
||||
*
|
||||
* @param uri the uri to open
|
||||
* @param context context
|
||||
*/
|
||||
private fun openLinkInBrowser(uri: Uri?, context: Context) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Actvity was not found for intent, $intent")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tries to open a link in a custom tab
|
||||
* falls back to browser if not possible
|
||||
*
|
||||
* @param uri the uri to open
|
||||
* @param context context
|
||||
*/
|
||||
private fun openLinkInCustomTab(uri: Uri, context: Context) {
|
||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
|
||||
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
||||
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
|
||||
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setNavigationBarColor(navigationbarColor)
|
||||
.setNavigationBarDividerColor(navigationbarDividerColor)
|
||||
.build()
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(colorSchemeParams)
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, uri)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity was not found for intent $customTabsIntent")
|
||||
openLinkInBrowser(uri, context)
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "LinkHelper"
|
|
@ -182,7 +182,7 @@ class ListStatusAccessibilityDelegate(
|
|||
android.R.layout.simple_list_item_1,
|
||||
textLinks
|
||||
)
|
||||
) { _, which -> LinkHelper.openLink(links[which].link, host.context) }
|
||||
) { _, which -> host.context.openLink(links[which].link) }
|
||||
.show()
|
||||
.let { forceFocus(it.listView) }
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 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 com.keylesspalace.tusky.util
|
||||
|
||||
enum class Status {
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
data class NetworkState private constructor(
|
||||
val status: Status,
|
||||
val msg: String? = null
|
||||
) {
|
||||
companion object {
|
||||
val LOADED = NetworkState(Status.SUCCESS)
|
||||
val LOADING = NetworkState(Status.RUNNING)
|
||||
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
|
||||
}
|
||||
}
|
|
@ -29,6 +29,6 @@ open class NoUnderlineURLSpan(
|
|||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
LinkHelper.openLink(url, view.context)
|
||||
view.context.openLink(url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,27 +17,39 @@ fun randomAlphanumericString(count: Int): String {
|
|||
}
|
||||
|
||||
// We sort statuses by ID. Something we need to invent some ID for placeholder.
|
||||
// Not sure if inc()/dec() should be made `operator` or not
|
||||
|
||||
/**
|
||||
* "Increment" string so that during sorting it's bigger than [this].
|
||||
* "Increment" string so that during sorting it's bigger than [this]. Inverse operation to [dec].
|
||||
*/
|
||||
fun String.inc(): String {
|
||||
// We assume that we will stay in the safe range for now
|
||||
val builder = this.toCharArray()
|
||||
builder[lastIndex] = builder[lastIndex].inc()
|
||||
return String(builder)
|
||||
var i = builder.lastIndex
|
||||
|
||||
while (i >= 0) {
|
||||
if (builder[i] < 'z') {
|
||||
builder[i] = builder[i].inc()
|
||||
return String(builder)
|
||||
} else {
|
||||
builder[i] = '0'
|
||||
}
|
||||
i--
|
||||
}
|
||||
return String(
|
||||
CharArray(builder.size + 1) { index ->
|
||||
if (index == 0) '0' else builder[index - 1]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "Decrement" string so that during sorting it's smaller than [this].
|
||||
* "Decrement" string so that during sorting it's smaller than [this]. Inverse operation to [inc].
|
||||
*/
|
||||
fun String.dec(): String {
|
||||
if (this.isEmpty()) return this
|
||||
|
||||
val builder = this.toCharArray()
|
||||
var i = builder.lastIndex
|
||||
while (i > 0) {
|
||||
while (i >= 0) {
|
||||
if (builder[i] > '0') {
|
||||
builder[i] = builder[i].dec()
|
||||
return String(builder)
|
||||
|
@ -46,12 +58,7 @@ fun String.dec(): String {
|
|||
}
|
||||
i--
|
||||
}
|
||||
return if (builder[0] > '1') {
|
||||
builder[0] = builder[0].dec()
|
||||
String(builder)
|
||||
} else {
|
||||
String(builder.copyOfRange(1, builder.size))
|
||||
}
|
||||
return String(builder.copyOfRange(1, builder.size))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,15 +78,6 @@ fun String.isLessThan(other: String): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun String.idCompareTo(other: String): Int {
|
||||
return when {
|
||||
this === other -> 0
|
||||
this.length < other.length -> -1
|
||||
this.length > other.length -> 1
|
||||
else -> this.compareTo(other)
|
||||
}
|
||||
}
|
||||
|
||||
fun Spanned.trimTrailingWhitespace(): Spanned {
|
||||
var i = length
|
||||
do {
|
||||
|
|
|
@ -21,9 +21,9 @@ import android.view.LayoutInflater
|
|||
import com.google.android.material.card.MaterialCardView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.CardLicenseBinding
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.openLink
|
||||
|
||||
class LicenseCard
|
||||
@JvmOverloads constructor(
|
||||
|
@ -50,7 +50,7 @@ class LicenseCard
|
|||
binding.licenseCardLink.hide()
|
||||
} else {
|
||||
binding.licenseCardLink.text = link
|
||||
setOnClickListener { LinkHelper.openLink(link, context) }
|
||||
setOnClickListener { context.openLink(link) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,15 +15,11 @@
|
|||
|
||||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
|
@ -31,16 +27,12 @@ import com.keylesspalace.tusky.entity.Instance
|
|||
import com.keylesspalace.tusky.entity.StringField
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.getSampledBitmap
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -52,30 +44,26 @@ import retrofit2.Call
|
|||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HEADER_FILE_NAME = "header.png"
|
||||
private const val AVATAR_FILE_NAME = "avatar.png"
|
||||
|
||||
private const val TAG = "EditProfileViewModel"
|
||||
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
private val eventHub: EventHub,
|
||||
private val application: Application
|
||||
) : ViewModel() {
|
||||
|
||||
val profileData = MutableLiveData<Resource<Account>>()
|
||||
val avatarData = MutableLiveData<Resource<Bitmap>>()
|
||||
val headerData = MutableLiveData<Resource<Bitmap>>()
|
||||
val avatarData = MutableLiveData<Uri>()
|
||||
val headerData = MutableLiveData<Uri>()
|
||||
val saveData = MutableLiveData<Resource<Nothing>>()
|
||||
val instanceData = MutableLiveData<Resource<Instance>>()
|
||||
|
||||
private var oldProfileData: Account? = null
|
||||
|
||||
private val disposeables = CompositeDisposable()
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
fun obtainProfile() {
|
||||
if (profileData.value == null || profileData.value is Error) {
|
||||
|
@ -92,70 +80,30 @@ class EditProfileViewModel @Inject constructor(
|
|||
profileData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposeables)
|
||||
.addTo(disposables)
|
||||
}
|
||||
}
|
||||
|
||||
fun newAvatar(uri: Uri, context: Context) {
|
||||
val cacheFile = getCacheFileForName(context, AVATAR_FILE_NAME)
|
||||
fun getAvatarUri() = getCacheFileForName(AVATAR_FILE_NAME).toUri()
|
||||
|
||||
resizeImage(uri, context, AVATAR_SIZE, AVATAR_SIZE, cacheFile, avatarData)
|
||||
fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri()
|
||||
|
||||
fun newAvatarPicked() {
|
||||
avatarData.value = getAvatarUri()
|
||||
}
|
||||
|
||||
fun newHeader(uri: Uri, context: Context) {
|
||||
val cacheFile = getCacheFileForName(context, HEADER_FILE_NAME)
|
||||
|
||||
resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData)
|
||||
fun newHeaderPicked() {
|
||||
headerData.value = getHeaderUri()
|
||||
}
|
||||
|
||||
private fun resizeImage(
|
||||
uri: Uri,
|
||||
context: Context,
|
||||
resizeWidth: Int,
|
||||
resizeHeight: Int,
|
||||
cacheFile: File,
|
||||
imageLiveData: MutableLiveData<Resource<Bitmap>>
|
||||
) {
|
||||
|
||||
Single.fromCallable {
|
||||
val contentResolver = context.contentResolver
|
||||
val sourceBitmap = getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
||||
|
||||
if (sourceBitmap == null) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
// dont upscale image if its smaller than the desired size
|
||||
val bitmap =
|
||||
if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
|
||||
sourceBitmap
|
||||
} else {
|
||||
Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)
|
||||
}
|
||||
|
||||
if (!saveBitmapToFile(bitmap, cacheFile)) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
bitmap
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
imageLiveData.postValue(Success(it))
|
||||
},
|
||||
{
|
||||
imageLiveData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposeables)
|
||||
}
|
||||
|
||||
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>, context: Context) {
|
||||
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>) {
|
||||
|
||||
if (saveData.value is Loading || profileData.value !is Success) {
|
||||
return
|
||||
}
|
||||
|
||||
saveData.value = Loading()
|
||||
|
||||
val displayName = if (oldProfileData?.intentionallyUseDisplayName == newDisplayName) {
|
||||
null
|
||||
} else {
|
||||
|
@ -174,15 +122,15 @@ class EditProfileViewModel @Inject constructor(
|
|||
newLocked.toString().toRequestBody(MultipartBody.FORM)
|
||||
}
|
||||
|
||||
val avatar = if (avatarData.value is Success && avatarData.value?.data != null) {
|
||||
val avatarBody = getCacheFileForName(context, AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||
val avatar = if (avatarData.value != null) {
|
||||
val avatarBody = getCacheFileForName(AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||
MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val header = if (headerData.value is Success && headerData.value?.data != null) {
|
||||
val headerBody = getCacheFileForName(context, HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||
val header = if (headerData.value != null) {
|
||||
val headerBody = getCacheFileForName(HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||
MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody)
|
||||
} else {
|
||||
null
|
||||
|
@ -256,29 +204,12 @@ class EditProfileViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getCacheFileForName(context: Context, filename: String): File {
|
||||
return File(context.cacheDir, filename)
|
||||
}
|
||||
|
||||
private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
|
||||
|
||||
val outputStream: OutputStream
|
||||
|
||||
try {
|
||||
outputStream = FileOutputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.w(TAG, Log.getStackTraceString(e))
|
||||
return false
|
||||
}
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
IOUtils.closeQuietly(outputStream)
|
||||
|
||||
return true
|
||||
private fun getCacheFileForName(filename: String): File {
|
||||
return File(application.cacheDir, filename)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposeables.dispose()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun obtainInstance() {
|
||||
|
@ -293,7 +224,7 @@ class EditProfileViewModel @Inject constructor(
|
|||
instanceData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposeables)
|
||||
.addTo(disposables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.google.android.material.button.MaterialButton;
|
|||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
|
@ -66,11 +67,16 @@ public class QuoteInlineHelper {
|
|||
quoteUsername.setText(usernameText);
|
||||
}
|
||||
|
||||
private void setContent(Spanned content, List<Status.Mention> mentions, List<Emoji> emojis,
|
||||
LinkListener listener) {
|
||||
private void setContent(
|
||||
Spanned content,
|
||||
List<Status.Mention> mentions,
|
||||
List<HashTag> tags,
|
||||
List<Emoji> emojis,
|
||||
LinkListener listener
|
||||
) {
|
||||
Spanned singleLineText = SpannedTextHelper.replaceSpanned(content);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(singleLineText, emojis, quoteContent, statusDisplayOptions.animateEmojis());
|
||||
LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, listener);
|
||||
LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, tags, listener);
|
||||
}
|
||||
|
||||
private void setAvatar(String url, @Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
|
||||
|
@ -117,8 +123,13 @@ public class QuoteInlineHelper {
|
|||
Account account = quoteStatus.getAccount();
|
||||
setDisplayName(account.getName(), account.getEmojis());
|
||||
setUsername(account.getUsername());
|
||||
setContent(quoteStatus.getContent(), quoteStatus.getMentions(),
|
||||
quoteStatus.getEmojis(), listener);
|
||||
setContent(
|
||||
quoteStatus.getContent(),
|
||||
quoteStatus.getMentions(),
|
||||
quoteStatus.getTags(),
|
||||
quoteStatus.getEmojis(),
|
||||
listener
|
||||
);
|
||||
setAvatar(account.getAvatar(), avatarRadius24dp, statusDisplayOptions);
|
||||
setOnClickListener(account.getId(), quoteStatus.getUrl());
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.keylesspalace.tusky.EditProfileActivity">
|
||||
tools:context=".EditProfileActivity">
|
||||
|
||||
<include
|
||||
android:id="@+id/includedToolbar"
|
||||
|
@ -37,17 +37,6 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/headerProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/headerPreview"
|
||||
app:layout_constraintEnd_toEndOf="@id/headerPreview"
|
||||
app:layout_constraintStart_toStartOf="@id/headerPreview"
|
||||
app:layout_constraintTop_toTopOf="@id/headerPreview" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatarPreview"
|
||||
android:layout_width="80dp"
|
||||
|
@ -71,18 +60,6 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/headerPreview"
|
||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/avatarProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatarPreview"
|
||||
app:layout_constraintEnd_toEndOf="@id/avatarPreview"
|
||||
app:layout_constraintStart_toStartOf="@id/avatarPreview"
|
||||
app:layout_constraintTop_toTopOf="@id/avatarPreview" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentContainer"
|
||||
android:layout_width="@dimen/timeline_width"
|
||||
|
|
|
@ -94,6 +94,16 @@
|
|||
license:link="https://square.github.io/retrofit/"
|
||||
license:name="Retrofit" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
license:license="@string/license_apache_2"
|
||||
license:link="https://github.com/google/gson"
|
||||
license:name="Gson" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -174,6 +184,16 @@
|
|||
license:link="https://github.com/CanHub/Android-Image-Cropper"
|
||||
license:name="Android Image Cropper" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
license:license="@string/license_apache_2"
|
||||
license:link="https://github.com/penfeizhou/APNG4Android"
|
||||
license:name="APNG4Android" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -242,7 +262,6 @@
|
|||
android:layout_marginTop="12dp"
|
||||
android:textSize="12sp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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/activity_view_thread"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="com.keylesspalace.tusky.ModalTimelineActivity">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<include
|
||||
android:id="@+id/includedToolbar"
|
||||
layout="@layout/toolbar_basic" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/contentFrame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floating_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/action_compose"
|
||||
app:layout_anchor="@id/contentFrame"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:srcCompat="@drawable/ic_create_24dp" />
|
||||
|
||||
<include layout="@layout/item_status_bottom_sheet" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<net.accelf.yuito.QuickTootView
|
||||
android:id="@+id/viewQuickToot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0" />
|
||||
|
||||
</LinearLayout>
|
|
@ -18,18 +18,18 @@
|
|||
layout="@layout/toolbar_basic" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floating_btn"
|
||||
android:id="@+id/floatingBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/action_compose"
|
||||
app:layout_anchor="@id/fragment_container"
|
||||
app:layout_anchor="@id/fragmentContainer"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:srcCompat="@drawable/ic_create_24dp" />
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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/activity_view_thread"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="com.keylesspalace.tusky.ViewTagActivity">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<include layout="@layout/item_status_bottom_sheet" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<net.accelf.yuito.QuickTootView
|
||||
android:id="@+id/viewQuickToot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0" />
|
||||
|
||||
</LinearLayout>
|
|
@ -37,7 +37,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:entries="@array/poll_duration_names"
|
||||
app:layout_constraintBottom_toBottomOf="@id/addChoiceButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/addChoiceButton"
|
||||
|
|
|
@ -72,12 +72,13 @@
|
|||
android:layout_height="32dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_accept"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/rejectButton"
|
||||
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
|
||||
app:layout_constraintTop_toTopOf="@id/avatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:srcCompat="@drawable/ic_check_24dp" />
|
||||
|
||||
<ImageButton
|
||||
|
@ -87,12 +88,12 @@
|
|||
android:layout_height="32dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_reject"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
|
||||
app:layout_constraintTop_toTopOf="@id/avatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:srcCompat="@drawable/ic_reject_24dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
android:title="@string/action_open_in_web"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/action_open_as"
|
||||
android:title="@string/action_open_as"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item android:id="@+id/action_mute"
|
||||
android:title="@string/action_mute"
|
||||
app:showAsAction="never" />
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue