Compare commits

..

No commits in common. "4eb56b713c1d04e24536eaeab336023f18ca33f8" and "f5110420a82091f27e91037b6475d9d6c5eb5b10" have entirely different histories.

264 changed files with 3317 additions and 12029 deletions

View File

@ -10,7 +10,7 @@ before_script:
- yes | sdkmanager "ndk-bundle" - yes | sdkmanager "ndk-bundle"
- export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk-bundle - export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk-bundle
- export ANDROID_NDK_HOME=$ANDROID_NDK_ROOT - export ANDROID_NDK_HOME=$ANDROID_NDK_ROOT
- sed -i "s/blue/\/\/blue/" app/build.gradle - sed -i "s/blue {}//" app/build.gradle
- sed -i "s/\/\/abortOnError/abortOnError/" app/build.gradle - sed -i "s/\/\/abortOnError/abortOnError/" app/build.gradle
# - sed -i "s/debug {}//" app/build.gradle # - sed -i "s/debug {}//" app/build.gradle
before_cache: before_cache:
@ -28,7 +28,7 @@ after_success:
- $ANDROID_HOME/build-tools/28.0.3/apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass "pass:android" --key-pass "pass:android" --in $(find -name "*-green-release-unsigned.apk") --out husky-green-release.apk - $ANDROID_HOME/build-tools/28.0.3/apksigner sign --ks debug.keystore --ks-key-alias androiddebugkey --ks-pass "pass:android" --key-pass "pass:android" --in $(find -name "*-green-release-unsigned.apk") --out husky-green-release.apk
- wget https://raw.githubusercontent.com/FWGS/uploadtool/master/upload.sh - wget https://raw.githubusercontent.com/FWGS/uploadtool/master/upload.sh
- chmod +x upload.sh - chmod +x upload.sh
- GITHUB_TOKEN=$GH_TOKEN ./upload.sh husky-green-debug.apk husky-green-release.apk - ./upload.sh husky-green-debug.apk husky-green-release.apk
branches: branches:
except: except:
# Do not build tags that we create when we upload to GitHub Releases # Do not build tags that we create when we upload to GitHub Releases

View File

@ -27,7 +27,7 @@ Tusky is quote, unquote, `... a beautiful Android client for [Mastodon](https://
If you have any bug reports, feature requests or questions please open an issue or send us a post at [Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)! If you have any bug reports, feature requests or questions please open an issue or send us a post at [Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)!
For translating Tusky into your language, visit https://weblate.tusky.app. For translating Tusky into your language, visit https://weblate.tusky.app/.\
For translating Husky, visit https://l10n.mentality.rip. For translating Husky, visit https://l10n.mentality.rip.
### Head of development ### Head of development

View File

@ -113,40 +113,41 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
ext.lifecycleVersion = "2.2.0" ext.lifecycleVersion = "2.2.0"
ext.roomVersion = '2.2.5' ext.roomVersion = '2.2.5'
ext.retrofitVersion = '2.9.0' ext.retrofitVersion = '2.9.0'
ext.okhttpVersion = '4.8.1' ext.okhttpVersion = '4.7.2'
ext.glideVersion = '4.11.0' ext.glideVersion = '4.11.0'
ext.daggerVersion = '2.28.3' ext.daggerVersion = '2.27'
ext.materialdrawerVersion = '8.1.4' ext.materialdrawerVersion = '8.1.2'
// if libraries are changed here, they should also be changed in LicenseActivity // if libraries are changed here, they should also be changed in LicenseActivity
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.3.1" implementation "androidx.core:core-ktx:1.3.0"
implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.appcompat:appcompat:1.2.0-rc01"
implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.exifinterface:exifinterface:1.2.0" implementation "androidx.exifinterface:exifinterface:1.2.0"
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.preference:preference:1.1.1" implementation "androidx.preference:preference:1.1.1"
implementation "androidx.sharetarget:sharetarget:1.0.0" implementation "androidx.sharetarget:sharetarget:1.0.0"
implementation "androidx.emoji:emoji:1.1.0" implementation "androidx.emoji:emoji:1.1.0-rc01"
implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0-rc01"
implementation "androidx.emoji:emoji-bundled:1.1.0" implementation "androidx.emoji:emoji-bundled:1.1.0-rc01"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.paging:paging-runtime-ktx:2.1.1"
implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.work:work-runtime:2.4.0" implementation "androidx.work:work-runtime:2.3.4"
implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
implementation "com.google.android.material:material:1.2.0" implementation "com.google.android.material:material:1.1.0"
implementation 'com.google.android:flexbox:2.0.1' implementation 'com.google.android:flexbox:2.0.1'
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
@ -177,9 +178,9 @@ dependencies {
implementation "com.github.connyduck:sparkbutton:4.0.0" implementation "com.github.connyduck:sparkbutton:4.0.0"
implementation 'com.github.piasy:BigImageViewer:1.7.0' implementation 'com.github.piasy:BigImageViewer:1.6.5'
implementation 'com.github.piasy:GlideImageLoader:1.7.0' implementation 'com.github.piasy:GlideImageLoader:1.6.5'
implementation 'com.github.piasy:GlideImageViewFactory:1.7.0' implementation 'com.github.piasy:GlideImageViewFactory:1.6.5'
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion" implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
@ -196,7 +197,7 @@ dependencies {
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0"
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
androidTestImplementation "androidx.test.ext:junit:1.1.1" androidTestImplementation "androidx.test.ext:junit:1.1.1"
debugImplementation "im.dino:dbinspector:4.0.0@aar" debugImplementation "im.dino:dbinspector:4.0.0@aar"

View File

@ -55,9 +55,6 @@
public static *** v(...); public static *** v(...);
public static *** i(...); public static *** i(...);
} }
-assumenosideeffects class java.lang.String {
public static java.lang.String format(...);
}
# remove some kotlin overhead # remove some kotlin overhead
-assumenosideeffects class kotlin.jvm.internal.Intrinsics { -assumenosideeffects class kotlin.jvm.internal.Intrinsics {
@ -65,3 +62,8 @@
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
static void throwUninitializedPropertyAccessException(java.lang.String); static void throwUninitializedPropertyAccessException(java.lang.String);
} }
# without this emoji font downloading fails with AbstractMethodError
-keep class * extends android.os.AsyncTask {
public *;
}

View File

@ -1,897 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "0a5f8f196d357a01b8b571098ea32431",
"entities": [
{
"tableName": "TootEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "descriptions",
"columnName": "descriptions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToText",
"columnName": "inReplyToText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToUsername",
"columnName": "inReplyToUsername",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "formattingSyntax",
"columnName": "formattingSyntax",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "markdownMode",
"columnName": "markdownMode",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"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, `notificationsStreamingEnabled` 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, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` 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, `defaultFormattingSyntax` 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": "notificationsStreamingEnabled",
"columnName": "notificationsStreamingEnabled",
"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": "notificationsEmojiReactions",
"columnName": "notificationsEmojiReactions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsChatMessages",
"columnName": "notificationsChatMessages",
"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
},
{
"fieldPath": "defaultFormattingSyntax",
"columnName": "defaultFormattingSyntax",
"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, `chatLimit` INTEGER, 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
},
{
"fieldPath": "chatLimit",
"columnName": "chatLimit",
"affinity": "INTEGER",
"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, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `pleroma` TEXT, 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": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"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": "pleroma",
"columnName": "pleroma",
"affinity": "TEXT",
"notNull": false
}
],
"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_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` 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.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.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ChatEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `lastMessageId` TEXT, PRIMARY KEY(`localId`, `chatId`))",
"fields": [
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chatId",
"columnName": "chatId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"localId",
"chatId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ChatMessageEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))",
"fields": [
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chatId",
"columnName": "chatId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachment",
"columnName": "attachment",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"localId",
"messageId"
],
"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, '0a5f8f196d357a01b8b571098ea32431')"
]
}
}

View File

@ -78,39 +78,46 @@ class TimelineDAOTest {
fun cleanup() { fun cleanup() {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000 val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000
val oldThisAccount = makeStatus( val oldByThisAccount = makeStatus(
statusId = 5, statusId = 5,
createdAt = oldDate createdAt = oldDate
) )
val oldAnotherAccount = makeStatus( val oldByAnotherAccount = makeStatus(
statusId = 10, statusId = 10,
createdAt = oldDate, createdAt = oldDate,
accountId = 2 authorServerId = "100"
) )
val recentThisAccount = makeStatus( val oldForAnotherAccount = makeStatus(
accountId = 2,
statusId = 20,
authorServerId = "200",
createdAt = oldDate
)
val recentByThisAccount = makeStatus(
statusId = 30, statusId = 30,
createdAt = System.currentTimeMillis() createdAt = System.currentTimeMillis()
) )
val recentAnotherAccount = makeStatus( val recentByAnotherAccount = makeStatus(
statusId = 60, statusId = 60,
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
accountId = 2 authorServerId = "200"
) )
for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) { for ((status, author, reblogAuthor) in listOf(oldByThisAccount, oldByAnotherAccount,
oldForAnotherAccount, recentByThisAccount, recentByAnotherAccount)) {
timelineDao.insertInTransaction(status, author, reblogAuthor) timelineDao.insertInTransaction(status, author, reblogAuthor)
} }
timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL) timelineDao.cleanup(1, "20", now - TimelineRepository.CLEANUP_INTERVAL)
assertEquals( assertEquals(
listOf(recentThisAccount), listOf(recentByAnotherAccount, recentByThisAccount, oldByThisAccount),
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() } .map { it.toTriple() }
) )
assertEquals( assertEquals(
listOf(recentAnotherAccount), listOf(oldForAnotherAccount),
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet() timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
.map { it.toTriple() } .map { it.toTriple() }
) )
@ -210,8 +217,7 @@ class TimelineDAOTest {
application = "application$accountId", application = "application$accountId",
reblogServerId = if (reblog) (statusId * 100).toString() else null, reblogServerId = if (reblog) (statusId * 100).toString() else null,
reblogAccountId = reblogAuthor?.serverId, reblogAccountId = reblogAuthor?.serverId,
poll = null, poll = null
muted = false
) )
return Triple(status, author, reblogAuthor) return Triple(status, author, reblogAuthor)
} }
@ -240,8 +246,7 @@ class TimelineDAOTest {
application = null, application = null,
reblogServerId = null, reblogServerId = null,
reblogAccountId = null, reblogAccountId = null,
poll = null, poll = null
muted = false
) )
} }

View File

@ -22,7 +22,7 @@
<string name="about_project_site">موقع المشروع :\n <string name="about_project_site">موقع المشروع :\n
https://huskyapp.dev</string> https://husky.fwgs.ru</string>

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -1,14 +0,0 @@
<resources>
<string name="action_login">ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ</string>
<string name="add_account_description">ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵎⴰⵚⵟⵓⴷⵓⵏ</string>
</resources>

View File

@ -1,62 +0,0 @@
<resources>
<string name="license_description">টাস্কি নিম্নলিখিত ওপেন সোর্স প্রকল্প থেকে কোড এবং সম্পদ রয়েছে:</string>
<string name="restart_emoji">এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে</string>
<string name="about_tusky_account">টাস্কির প্রোফাইল</string>
<string name="about_tusky_license">টাস্কি মুক্ত এবং ওপেন সোর্স সফ্টওয়্যার। এটি GNU জেনারেল পাবলিক লাইসেন্স সংস্করণ 3 এর অধীনে লাইসেন্সযুক্ত। আপনি এখানে লাইসেন্স দেখতে পারেন: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_tusky_version">টাস্কি %s</string>
<string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string>
<string name="about_project_site">প্রকল্প ওয়েবসাইট:
\nhttps://huskyapp.dev</string>
<string name="about_bug_feature_request_site">বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ:
\nhttps://git.mentality.rip/FWGS/Husky/issues</string>
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
<string name="action_login">মাস্টোডনের সঙ্গে লগইন করো</string>
<string name="warning_scheduling_interval">মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে।</string>
<string name="dialog_whats_an_instance">"কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন shitposter.club, blob.cat, expired.mentality.rip, এবং &lt;a href=\"https://fediverse.network/pleroma?count=peers\"&gt; আরও! &lt;/a&gt;
\n
\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন।
\n
\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন।
\n
\nআরো তথ্য &lt;a href=\"https://joinmastodon.org\"&gt; joinmastodon.org &lt;/a&gt; এ পাওয়া যেতে পারে। "<a href="https://fediverse.network/pleroma?count=peers">more!</a>
\n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to
join and create an account there.\n\nAn instance is a single place where your account is
hosted, but you can easily communicate with and follow folks on other instances as though
you were on the same site.
\n\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>.
</string>
</resources>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">প্রকল্প ওয়েবসাইট: <string name="about_project_site">প্রকল্প ওয়েবসাইট:
\nhttps://huskyapp.dev</string> \nhttps://husky.fwgs.ru</string>
@ -33,23 +33,20 @@
<string name="action_login">াস্টোডনের সঙ্গে লগইন করো</string> <string name="action_login">্যাস্টোডোন সঙ্গে লগইন করুন</string>
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string> <string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
<string name="warning_scheduling_interval">মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে।</string>
<string name="dialog_whats_an_instance">কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন shitposter.club, blob.cat, expired.mentality.rip, এবং <a href="https://fediverse.network/pleroma?count=peers"> আরও! </a> <string name="dialog_whats_an_instance">কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন shitposter.club, blob.cat, expired.mentality.rip, এবং <a href="https://fediverse.network/pleroma?count=peers"> আরও! </a>
\n \n
\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। \nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন।
\n \n
\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। \nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন।
\n \n
\nআরো তথ্য <a href="https://joinmastodon.org"> joinmastodon.org </a> এ পাওয়া যেতে পারে। </string> \nআরো তথ্য <a href="https://joinmastodon.org"> joinmastodon.org </a> এ পাওয়া যেতে পারে। </string>

View File

@ -25,7 +25,7 @@
<string name="about_project_site"> <string name="about_project_site">
Lloc web del projecte:\n Lloc web del projecte:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -16,15 +16,12 @@
<string name="license_description">Husky obsahuje kód a zdroje z následujících otevřených projektů:</string> <string name="license_description">Husky obsahuje kód a zdroje z následujících otevřených projektů:</string>
<string name="about_powered_by_tusky">Powered by Husky</string>
<string name="about_project_site"> Webová stránka projektu:\n <string name="about_project_site"> Webová stránka projektu:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -42,9 +39,6 @@
<string name="add_account_description">Přidat nový účet Pleroma</string> <string name="add_account_description">Přidat nový účet Pleroma</string>
<string name="warning_scheduling_interval">Pleroma neumožňuje pracovat s intervalem menším než 5 minut.</string>

View File

@ -18,7 +18,7 @@
<string name="about_project_site"> Gwefan y prosiect:\n <string name="about_project_site"> Gwefan y prosiect:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -1,6 +1,6 @@
<resources> <resources>
<string name="about_tusky_license">Husky ist freie und quelloffene Software. Es ist lizenziert unter der GNU General Public License Version 3. Du kannst dir die Lizenz hier anschauen: https://www.gnu.org/licenses/gpl-3.0.de.html</string> <string name="about_tusky_license">Husky ist freie eine quelloffene Software. Es ist lizenziert unter der GNU General Public License Version 3. Du kannst dir die Lizenz hier anschauen: https://www.gnu.org/licenses/gpl-3.0.de.html</string>
<string name="about_tusky_account">Huskys Profil</string> <string name="about_tusky_account">Huskys Profil</string>
@ -12,7 +12,7 @@
<string name="license_description">Husky enthält Code und Inhalte von den folgenden Open-Source-Projekten:</string> <string name="license_description">Husky enthält Code und Inhalte von den folgenden Open-Source-Projekten:</string>
<string name="about_tusky_version">test %s</string> <string name="about_tusky_version">Husky %s</string>
<string name="about_powered_by_tusky">Angetrieben durch Husky</string> <string name="about_powered_by_tusky">Angetrieben durch Husky</string>
@ -21,8 +21,8 @@
<string name="about_project_site">Website des Projekts: <string name="about_project_site">Website des Projekts:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="notification_favourite_format">%s favourited your post</string> <string name="notification_favourite_format">%s favourited your post</string>
<string name="action_access_scheduled_toot">Scheduled posts</string>
<string name="action_reply_to">Reply to</string>
</resources> </resources>

View File

@ -24,7 +24,7 @@
<string name="about_project_site"> Paĝaro de projekto:\n <string name="about_project_site"> Paĝaro de projekto:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -24,7 +24,7 @@
<string name="about_project_site"> Sitio del proyecto:\n <string name="about_project_site"> Sitio del proyecto:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -24,7 +24,7 @@
<string name="about_project_site"> Proiektuaren gunea:\n <string name="about_project_site"> Proiektuaren gunea:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -6,10 +6,10 @@
<string name="about_tusky_account">نمایهٔ تاسکی</string> <string name="about_tusky_account">نمایهٔ تاسکی</string>
<string name="restart_emoji">برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید</string> <string name="restart_emoji">شما برای اعمال این تغییرات به شروع مجدد برنامه نیاز دارید</string>
<string name="license_description">تاسکی کد و دارایی‌هایی از پروژه‌های نرم‌افزار آزاد زیر دارد:</string> <string name="license_description">تاسکی شامل کد و دارایی‌هایی از پروژه‌های آزاد زیر است:</string>
<string name="about_tusky_version">تاسکی %s</string> <string name="about_tusky_version">تاسکی %s</string>
@ -21,13 +21,13 @@
<string name="about_project_site">پایگاه وب پروژه : <string name="about_project_site">پایگاه وب پروژه:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>
<string name="about_bug_feature_request_site">گزارش مشکلات و درخواست ویژگی‌ها: <string name="about_bug_feature_request_site">گزارش مشکلات و درخواست ویژگی‌ها:
\n https://git.mentality.rip/FWGS/Husky/issues</string> \n https://git.mentality.rip/FWGS/Husky/issues</string>
@ -36,7 +36,7 @@
<string name="action_login">ورود با ماستودون</string> <string name="action_login">ورود با ماستودون</string>
<string name="add_account_description">افزودن حساب ماستودون جدید</string> <string name="add_account_description">افزودن حساب جدید ماستودون</string>
<string name="warning_scheduling_interval">ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد.</string> <string name="warning_scheduling_interval">ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد.</string>

View File

@ -24,7 +24,7 @@
<string name="about_project_site"> Site du projet :\n <string name="about_project_site"> Site du projet :\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -43,7 +43,7 @@
<string name="add_account_description">Ajouter un nouveau compte Pleroma</string> <string name="add_account_description">Ajouter un nouveau compte Pleroma</string>
<string name="warning_scheduling_interval">Lintervalle minimum de planification sur Pleroma est de5 minutes.</string> <string name="warning_scheduling_interval">Lintervalle minimum de planification sur Pleroma est de 5 minutes.</string>

View File

@ -1,56 +0,0 @@
<resources>
<string name="about_tusky_account">Próifíl Husky</string>
<string name="about_tusky_license">Is bogearraí foinse oscailte agus saor in aisce é Husky. Tá sé ceadúnaithe faoi Leagan 3. Ceadúnas Poiblí Ginearálta GNU 3. Is féidir leat an ceadúnas a fheiceáil anseo: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">Cumhachtaithe ag Husky</string>
<string name="about_tusky_version">Husky %s</string>
<string name="restart_emoji">Beidh ort Husky a atosú chun na hathruithe seo a chur i bhfeidhm</string>
<string name="license_description">Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Husky:</string>
<string name="about_project_site">Suíomh Gréasáin an tionscadail:
\n https://huskyapp.dev</string>
<string name="about_bug_feature_request_site">Tuarascálacha ar fhabhtanna &amp; iarratais ar ghnéithe:
\n https://git.mentality.rip/FWGS/Husky/issues</string>
<string name="action_login">Logáil isteach le Pleroma</string>
<string name="add_account_description">Cuir Cuntas Pleroma nua leis</string>
<string name="warning_scheduling_interval">Tá eatramh sceidealaithe íosta 5 nóiméad ag Pleroma.</string>
<string name="dialog_whats_an_instance">Is féidir seoladh nó fearann aon cháis a iontráil anseo, mar shampla shitposter.club, blob.cat, expired.mentality.rip, agus <a href="https://fediverse.network/pleroma?count=peers"> níos mó! </a>
\n
\nMura bhfuil cuntas agat fós, is féidir leat ainm an cháis ar mhaith leat a bheith páirteach ann agus cuntas a chruthú ann.
\n
\nIs áit amháin é sampla ina ndéantar do chuntas a óstáil, ach is féidir leat cumarsáid a dhéanamh go héasca le daoine eile agus iad a leanúint ar chásanna eile mar a bheadh tú ar an suíomh céanna.
\n
\nIs féidir tuilleadh faisnéise a fháil ag <a href="https://joinmastodon.org"> joinmastodon.org </a>. </string>
</resources>

View File

@ -1,11 +0,0 @@
<resources>
<string name="action_login">Clàraich a-steach le Pleroma</string>
</resources>

View File

@ -1,46 +1,11 @@
<resources> <resources>
<string name="license_description">टस्की में निम्नलिखित ओपन सोर्स परियोजनाओं से कोड और संपत्ति हैं:</string>
<string name="restart_emoji">इन परिवर्तनों को लागू करने के लिए आपको टस्की को पुनः आरंभ करना होगा</string>
<string name="about_tusky_account">टस्की की प्रोफाइल</string>
<string name="about_tusky_license">टस्की स्वतंत्र और ओपन-सोर्स सॉफ्टवेयर है। यह GNU जनरल पब्लिक लाइसेंस संस्करण 3 के तहत लाइसेंस प्राप्त है। आप लाइसेंस यहां देख सकते हैं: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">टस्की द्वारा संचालित</string>
<string name="about_tusky_version">टस्की %s</string>
<string name="about_project_site">परियोजना की वेबसाइट:
\n https://huskyapp.dev</string>
<string name="about_bug_feature_request_site">बग रिपोर्ट और सुविधा अनुरोध:
\n https://git.mentality.rip/FWGS/Husky/issues</string>
<string name="action_login">हिंदी</string> <string name="action_login">हिंदी</string>
<string name="warning_scheduling_interval">मास्टोडन का न्यूनतम शेड्यूलिंग अंतराल 5 मिनट है।</string>
<string name="add_account_description">नया मास्टोडन खाता जोड़ें</string>
</resources> </resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_reply_to">जवाब दे</string>
<string name="action_emoji_reacted_by">किसने प्रतिक्रिया व्यक्त की</string>
<string name="action_emoji_react">प्रतिक्रिया</string>
<string name="action_emoji_unreact">प्रतिक्रिया निकालें</string>
</resources>

View File

@ -15,14 +15,14 @@
<string name="about_tusky_license">Husky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthetsz meg: https://www.gnu.org/licenses/gpl-3.0.en.html</string> <string name="about_tusky_license">Husky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthetsz meg: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">Husky által hajtva</string> <string name="about_powered_by_tusky">Husky által hatjva</string>
<string name="about_project_site"> Projekt honlapja:\n <string name="about_project_site"> Projekt honlapja:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -34,7 +34,7 @@
<string name="action_login">Bejelentkezés Pleroma-nal</string> <string name="action_login">Bejelentkezés Pleroma-al</string>
<string name="add_account_description">Új Pleroma fiók hozzáadása</string> <string name="add_account_description">Új Pleroma fiók hozzáadása</string>
@ -46,11 +46,11 @@
<string name="dialog_whats_an_instance">Bármely szerver címét beírhatod ide, mint shitposter.club, blob.cat, expired.mentality.rip, és <a href="https://fediverse.network/pleroma?count=peers">mások!</a> <string name="dialog_whats_an_instance">Bármely szerver címét beírhatod ide, mint shitposter.club, blob.cat, expired.mentality.rip, és <a href="https://fediverse.network/pleroma?count=peers">mások!</a>
\n \n
\nHa még nincs fiókod, beírhatod a címét ide annak a szervernek amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot. \nHa még nincs fiókod, beírhatod a címét ide annak a szervernek amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot.
\n \n
\nA szerver az a hely ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más szervereken lévő emberekkel, mintha ugyanazon az oldalon lennétek. \nA szerver az a hely ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más szervereken lévő emberekkel, mintha ugyanazon az oldalon lennétek.
\n \n
\nTöbb információt találhatsz itt: <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \nTöbb információt találhatsz itt: <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">Vefsvæði verkefnisins: <string name="about_project_site">Vefsvæði verkefnisins:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>

View File

@ -24,7 +24,7 @@
<string name="about_project_site">Sito web del progetto:\n <string name="about_project_site">Sito web del progetto:\n
https://huskyapp.dev</string> https://husky.fwgs.ru</string>
@ -44,12 +44,12 @@
<string name="dialog_whats_an_instance">L\'indirizzo o il dominio di qualsiasi istanza può essere inserito qui, come shitposter.club, blob.cat, expired.mentality.rip, e <a href="https://fediverse.network/pleroma?count=peers">altro!</a> <string name="dialog_whats_an_instance">L\'indirizzo o il dominio di qualsiasi istanza può essere inserito qui, come shitposter.club, blob.cat, expired.mentality.rip, e <a href="https://fediverse.network/pleroma?count=peers">altro!</a>
\n \n
\nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account. \nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account.
\n \n
\nUn\'istanza è il luogo dove l\'account è custodito, ma puoi facilmente comunicare e seguire gente su altre istanze come se fossero sullo stesso sito. \nUn\'istanza è il luogo dove l\'account è custodito, ma puoi facilmente comunicare e seguire gente su altre istanze come se fossero sullo stesso sito.
\n \n
\nPiù info possono essere trovate su <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \nPiù info possono essere trovate su <a href="https://joinmastodon.org">joinmastodon.org</a>.</string>
</resources> </resources>

View File

@ -21,7 +21,7 @@
<string name="about_project_site"> プロジェクトのWebサイト英語\n <string name="about_project_site"> プロジェクトのWebサイト英語\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -39,18 +39,15 @@
<string name="add_account_description">新しいPleromaアカウントを追加</string> <string name="add_account_description">新しいPleromaアカウントを追加</string>
<string name="warning_scheduling_interval">Pleromaにおける予約までの最小間隔は5分です。</string>
<string name="dialog_whats_an_instance">shitposter.club, blob.cat, expired.mentality.ripや<a href="https://fediverse.network/pleroma?count=peers">その他</a> のような、あらゆるインスタンスのアドレスやドメインを入力できます。 <string name="dialog_whats_an_instance">shitposter.club, mstdn.jp, pawoo.netや<!-- --><a href="https://fediverse.network/pleroma?count=peers">その他</a><!-- -->のような、あらゆるインスタンスのアドレスやドメインを入力できます。
\n \n
\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで そのインスタンスにアカウントを作成できます。 \nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで<!-- -->そのインスタンスにアカウントを作成できます。
\n \n
\nインスタンスはあなたのアカウントが提供される単独の場所ですが、他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。 \nインスタンスはあなたのアカウントが提供される単独の場所ですが、<!-- -->他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
\n \n
\nさらに詳しい情報は<a href="https://joinmastodon.org">joinmastodon.org</a>でご覧いただけます。 </string> \nさらに詳しい情報は<a href="https://joinmastodon.org">joinmastodon.org</a>でご覧いただけます。 </string>

View File

@ -13,7 +13,7 @@
<string name="about_project_site">Asmel Web n usenfaṛ: <string name="about_project_site">Asmel Web n usenfaṛ:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>

View File

@ -19,7 +19,7 @@
<string name="about_project_site">프로젝트 홈페이지: <string name="about_project_site">프로젝트 홈페이지:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>
@ -40,11 +40,22 @@
<string name="dialog_whats_an_instance">인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. shitposter.club, blob.cat, expired.mentality.rip 등이 있으며, 그 외에도 <a href="https://fediverse.network/pleroma?count=peers">더 많은 인스턴스</a>가 당신을 기다리고 있습니다! <string name="dialog_whats_an_instance">인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. shitposter.club, blob.cat, expired.mentality.rip 등이 있으며, 그 외에도 <a href="https://fediverse.network/pleroma?count=peers">더 많은 인스턴스</a>가 당신을 기다리고 있습니다!
\n \n
\n
\n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다. \n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다.
\n
\n \n
\n
\n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다. \n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다.
\n
\n \n
\n자세한 사항은 <a href="https://joinmastodon.org">joinmastodon.org</a>을 참조하세요. </string> \n
\n자세한 사항은 &lt;a href=“https://joinmastodon.org”&gt;joinmastodon.org&lt;/a&gt;을 참조하세요. <a href="https://fediverse.network/pleroma?count=peers">more!</a>
\n
\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there.
\n
\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site.
\n
\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
</resources> </resources>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">Projectwebsite:\n <string name="about_project_site">Projectwebsite:\n
https://huskyapp.dev</string> https://husky.fwgs.ru</string>
@ -50,7 +50,7 @@
\n \n
\nEen Mastodonserver (Engels: instance) is een computerserver waar jouw account zich bevindt (vergelijk het met een e-mailserver). Je kan eenvoudig mensen van andere servers volgen en met ze communiceren, alsof jullie met elkaar op dezelfde website zitten. \nEen Mastodonserver (Engels: instance) is een computerserver waar jouw account zich bevindt (vergelijk het met een e-mailserver). Je kan eenvoudig mensen van andere servers volgen en met ze communiceren, alsof jullie met elkaar op dezelfde website zitten.
\n \n
\n Meer informatie kun je vinden op <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \n Meer informatie kun je vinden op <a href="https://joinmastodon.org">joinmastodon.org</a>.</string>
</resources> </resources>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">Hjemmeside: <string name="about_project_site">Hjemmeside:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>
@ -44,13 +44,13 @@
<string name="dialog_whats_an_instance"><a href="https://fediverse.network/pleroma?count=peers">more!</a> <string name="dialog_whats_an_instance">Adressen eller domenet til en instans kan skrives inn her, for eksempel shitposter.club, blob.cat, expired.mentality.rip, og <a href="https://fediverse.network/pleroma?count=peers">flere!</a>!
\n \n
\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there. \nHvis du ikke har en konto, kan du skrive inn navnet på instansen du ønsker å opprette en konto på her.
\n \n
\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site. \nEn instans er en sted hvor kontoen din er registrert, men du kan enkelt kommunisere med og følge brukere på andre instanser som om dere er på den samme instansen.
\n \n
\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \nMer informasjon finner du på <a href="https://joinmastodon.org">joinmastodon.org</a>.</string>
</resources> </resources>

View File

@ -24,7 +24,7 @@
<string name="about_project_site"> Site web del projècte :\n <string name="about_project_site"> Site web del projècte :\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -22,7 +22,7 @@
<string name="about_project_site"> Strona projektu:\n <string name="about_project_site"> Strona projektu:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -41,18 +41,20 @@
<string name="add_account_description">Dodaj nowe Konto Pleroma</string> <string name="add_account_description">Dodaj nowe Konto Pleroma</string>
<string name="warning_scheduling_interval">Pleroma umożliwia wysłanie minimalnie 5 minut od zaplanowania.</string> <string name="warning_scheduling_interval">Pleroma umożliwia wysłanie minimalnie 5 minut od zaplanowania</string>
<string name="dialog_whats_an_instance">Tutaj można wprowadzić domenę lub adres instancji, np. shitposter.club, blob.cat, expired.mentality.rip, i <a href="https://fediverse.network/pleroma?count=peers">wiele więcej!</a> <string name="dialog_whats_an_instance">Tutaj można wprowadzić domenę lub adres instancji,
\n np. shitposter.club, blob.cat, expired.mentality.rip, i
\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrować. <a href="https://fediverse.network/pleroma?count=peers">wiele więcej!</a>
\n \n\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrować.
\nInstancja jest miejscem, na którym znajduje się twoje konto, lecz komunikując się z innymi serwerami, działa tak, jakby były jednym portalem. Instancja jest miejscem, na którym znajduje się twoje konto,
\n lecz komunikując się z innymi serwerami, działa tak,
\nWięcej informacji można znaleźć na <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> jakby były jednym portalem.
Więcej informacji można znaleźć na <a href="https://joinmastodon.org">joinmastodon.org</a>.
</string>
</resources> </resources>

View File

@ -59,6 +59,6 @@
<string name="action_enable_formatting_syntax">Włącz %s</string> <string name="action_enable_formatting_syntax">Włącz %s</string>
<string name="send_toot_notification_saved_content">Kopia postu została zapisana jako szkic</string> <string name="send_toot_notification_saved_content">Kopia postu została zapisana jako szkic</string>
<string name="error_sender_account_gone">Wysyłanie postu nie powiodło się.</string> <string name="error_sender_account_gone">Wysyłanie postu nie powiodło się.</string>
<string name="pref_title_alway_open_spoiler">Zawsze rozwijaj posty z ostrzeżeniami o zawartości</string> <string name="pref_title_alway_open_spoiler">Nie ukrywaj zawartości multimedialnej oznaczonej jako wrażliwa</string>
<string name="action_access_scheduled_toot">Zaplanowane posty</string> <string name="action_access_scheduled_toot">Zaplanowane posty</string>
</resources> </resources>

View File

@ -11,7 +11,7 @@
<string name="about_tusky_account">Perfil do Husky</string> <string name="about_tusky_account">Perfil do Husky</string>
<string name="restart_emoji">É necessário reiniciar o aplicativo para aplicar as alterações</string> <string name="restart_emoji">É necessário reiniciar o Husky para aplicar essas mudanças</string>
<string name="license_description">O Husky contém código e recursos dos seguintes projetos de código aberto:</string> <string name="license_description">O Husky contém código e recursos dos seguintes projetos de código aberto:</string>
@ -24,7 +24,7 @@
<string name="about_project_site">Site do projeto:\n <string name="about_project_site">Site do projeto:\n
https://huskyapp.dev</string> https://husky.fwgs.ru</string>

View File

@ -25,7 +25,7 @@
<string name="about_project_site"> <string name="about_project_site">
Веб-сайт проекта:\n Веб-сайт проекта:\n
https://huskyapp.dev</string> https://husky.fwgs.ru</string>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="chats">Чаты</string>
<string name="action_mark_as_read">Пометить как прочитанное</string>
<string name="action_reply_to">Ответ на</string> <string name="action_reply_to">Ответ на</string>
<!--<string name="action_mute_conversation">Заглушить разговор</string> <!--<string name="action_mute_conversation">Заглушить разговор</string>
<string name="action_unmute_conversation">Отменить глушение разговора</string>--> <string name="action_unmute_conversation">Отменить глушение разговора</string>-->
@ -19,31 +17,7 @@
<string name="notification_emoji_format">%s среагировал с %s на ваш пост</string> <string name="notification_emoji_format">%s среагировал с %s на ваш пост</string>
<string name="notification_emoji_name">Эмодзи реакции</string> <string name="notification_emoji_name">Эмодзи реакции</string>
<string name="notification_emoji_description">Уведомления о новых эмодзи реакциях</string> <string name="notification_emoji_description">Уведомления о новых эмодзи реакциях</string>
<string name="notification_chat_message_format">%s отправил вам сообщение</string>
<string name="notification_chat_message_name">Сообщения</string>
<string name="notification_chat_message_description">Уведомления о новых сообщениях</string>
<string name="pref_title_default_formatting">Синтаксис форматирования по умолчанию(если поддерживается)</string> <string name="pref_title_default_formatting">Синтаксис форматирования по умолчанию(если поддерживается)</string>
<string name="pref_title_notification_filter_emoji">на мои посты отреагировали</string> <string name="pref_title_notification_filter_emoji">на мои посты отреагировали</string>
<string name="pref_title_notification_filter_chat_messages">получено новое сообщение</string>
<string name="pref_title_hide_muted_users">Скрывать заглушенных пользователей</string> <string name="pref_title_hide_muted_users">Скрывать заглушенных пользователей</string>
<string name="error_sticker_fetch">Произошла ошибка при загрузке стикера</string>
<string name="action_send_public">ОТПРАВИТЬ!</string>
<string name="action_sticker">Стикеры</string>
<string name="action_schedule_toot">Отложить пост</string>
<string name="pref_title_enable_experimental_stickers">Включить эксперементальные стикеры Pleroma-FE (если доступны)</string>
<string name="action_send">ОТПРАВИТЬ</string>
<string name="description_status_reblogged">Повторенно</string>
<string name="dialog_delete_toot_warning">Удалить запись\?</string>
<string name="action_hide_reblogs">Скрыть повторения</string>
<string name="pref_title_enable_big_emojis">Включить большие пользовательские эмодзи</string>
<string name="action_open_toot">Открыть запись</string>
<string name="action_reblog">Повторить</string>
<string name="action_show_reblogs">Показать повторения</string>
<string name="reblog_private">Повторить в оригинальной версии</string>
<string name="action_open_reblogged_by">Показать повторения</string>
<string name="action_unreblog">Отменить повторение</string>
<string name="action_access_scheduled_toot">Записи по расписанию</string>
<string name="unreblog_private">Удалить повторение</string>
<string name="compose_shortcut_long_label">Сделать запись</string>
<string name="link">Ссылка</string>
</resources> </resources>

View File

@ -1,56 +0,0 @@
<resources>
<string name="about_tusky_account">टस्कीवर्यस्य व्यक्तिगतविवरणम्</string>
<string name="about_tusky_license">टस्कीत्यनावृतस्रोतो निःशुल्कतन्त्रांशः। GNU General Public License Version 3 इत्यनेनाऽनुज्ञापितः। अत्राऽनुज्ञापत्रं द्रष्टुं शक्यते:-https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">टस्कीत्यनेनाऽऽश्रितः</string>
<string name="about_tusky_version">टस्की %s</string>
<string name="restart_emoji">पुनश्च टस्कीप्रारम्भोऽपेक्षितो वर्तते परिवर्तनानुसरेण चलितुम्</string>
<string name="license_description">टस्कीत्यस्मिन्निम्नलिखितेभ्योऽनावृतस्रोतःप्रकल्पेभ्यो विध्यादेशाः सन्ति:</string>
<string name="about_project_site">प्रकल्पस्य जालसूत्रम् :
\n https://huskyapp.dev</string>
<string name="about_bug_feature_request_site">अशुद्धीनामावेदनं वैशिष्ट्यनिवेदनञ्च
\n https://git.mentality.rip/FWGS/Husky/issues</string>
<string name="action_login">मास्टुडोनमाध्यमेन सम्प्रविश्यताम्</string>
<string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string>
<string name="warning_scheduling_interval">मास्टोडोने पञ्चनिमेषपरिमितो न्यूनतमः कालबद्धसमयः ।</string>
<string name="dialog_whats_an_instance">कस्याऽपि विशिष्टस्थलस्य सङ्केतसूत्रमत्र टङ्कयितुं शक्यते shitposter.club, blob.cat, expired.mentality.rip, तथेैव<a href="https://fediverse.network/pleroma?count=peers">अधिकम्</a>
\n
\nयदि युष्माकं व्यक्तिगतलेखाऽत्र न वर्तते तर्हि तस्य विशिष्टस्थलस्य नाम टङ्कयित्वा तत्र निर्मातुं शक्नुथ ।
\n
\nविशिष्टस्थलमित्युक्ते स्थलमेकं यत्र युष्माकं लेखाः आश्रिताः, किन्तु साफल्येनैवाऽन्यविशिष्टस्थलीयैः सह सम्पर्कयितुं शक्यते ।
\n
\nअधिकमत्र प्राप्यते <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
</resources>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">Spletna stran projekta: <string name="about_project_site">Spletna stran projekta:
\nhttps://huskyapp.dev</string> \nhttps://husky.fwgs.ru</string>

View File

@ -22,7 +22,7 @@
<string name="about_project_site"> Tuskys webbsida:\n <string name="about_project_site"> Tuskys webbsida:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -24,7 +24,7 @@
<string name="action_send_public">SKICKA!</string> <string name="action_send_public">SKICKA!</string>
<string name="action_reply_to">Svara till</string> <string name="action_reply_to">Svara till</string>
<string name="hint_website">Applikationswebbplats</string> <string name="hint_website">Applikationswebbplats</string>
<string name="moderator">Moderatorn</string> <string name="moderator">Moderator</string>
<string name="notification_emoji_format">%s reagerade med %s på ditt inlägg</string> <string name="notification_emoji_format">%s reagerade med %s på ditt inlägg</string>
<string name="notification_emoji_description">Aviseringar på nya emoji-reaktioner</string> <string name="notification_emoji_description">Aviseringar på nya emoji-reaktioner</string>
<string name="pref_title_default_formatting">Syntax på formatteringsstandard (om instansen stödjer det)</string> <string name="pref_title_default_formatting">Syntax på formatteringsstandard (om instansen stödjer det)</string>
@ -39,31 +39,4 @@
<string name="dialog_delete_toot_warning">Ta bort detta inlägg\?</string> <string name="dialog_delete_toot_warning">Ta bort detta inlägg\?</string>
<string name="action_open_reblogger">Öppna avsändaren av repeteringen</string> <string name="action_open_reblogger">Öppna avsändaren av repeteringen</string>
<string name="description_status_reblogged">Repeterat</string> <string name="description_status_reblogged">Repeterat</string>
<string name="dialog_redraft_toot_warning">Radera och skriva en nytt inlägg\?</string>
<string name="error_sender_account_gone">Fel vid sändning av inlägg.</string>
<string name="notification_reblog_format">%s upprepade ditt inlägg</string>
<string name="notification_favourite_format">%s favoriserade ditt inlägg</string>
<string name="notification_boost_name">Upprepningar</string>
<string name="notification_favourite_description">Aviseringar när dina inlägg blir favoriserade</string>
<string name="pref_title_notification_filter_reblogs">mina inlägg är repeterade</string>
<string name="pref_title_show_boosts">Visa upprepningar</string>
<string name="pref_title_alway_open_spoiler">Expandera alltid inlägg med innehållsvarningar</string>
<plurals name="reblogs">
<item quantity="one"><b>%s</b> Repeterade</item>
<item quantity="other"><b>%s</b> Repeterades</item>
</plurals>
<string name="send_status_link_to">Dela inläggs-URL till…</string>
<string name="send_status_content_to">Dela inlägg till…</string>
<string name="send_toot_notification_title">Skickar inlägg…</string>
<string name="send_toot_notification_channel_name">Skickar inläggen</string>
<string name="send_toot_notification_saved_content">En kopia av inlägget har sparats i dina utkast</string>
<string name="status_share_content">Dela innehåll av inlägg</string>
<string name="status_share_link">Dela länk till inlägg</string>
<string name="status_boosted_format">%s repeterade</string>
<string name="title_scheduled_toot">Schemalagda inlägg</string>
<string name="title_reblogged_by">Upprepad av</string>
<string name="title_view_thread">Posta</string>
<string name="notification_boost_description">Aviseringar när dina inlägg blir upprepade</string>
<string name="pref_title_confirm_reblogs">Visa en bekräftelsedialog innan du repeterar</string>
<string name="send_toot_notification_error_title">Fel vid sändning av inlägg</string>
</resources> </resources>

View File

@ -19,7 +19,7 @@
<string name="about_project_site"> திட்டத்தின் வலைத்தளம்:\n <string name="about_project_site"> திட்டத்தின் வலைத்தளம்:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -22,7 +22,7 @@
<string name="about_project_site">เว็บไซต์โปรเจกต์: <string name="about_project_site">เว็บไซต์โปรเจกต์:
\nhttps://huskyapp.dev</string> \nhttps://husky.fwgs.ru</string>

View File

@ -64,5 +64,4 @@
<string name="error_sticker_fetch">เกิดข้อผิดพลาดขณะดึงข้อมูลสติกเกอร์</string> <string name="error_sticker_fetch">เกิดข้อผิดพลาดขณะดึงข้อมูลสติกเกอร์</string>
<string name="action_emoji_unreact">ลบโต้ตอบแบบเอโมจิ</string> <string name="action_emoji_unreact">ลบโต้ตอบแบบเอโมจิ</string>
<string name="action_emoji_reacted_by">ผู้โต้ตอบ</string> <string name="action_emoji_reacted_by">ผู้โต้ตอบ</string>
<string name="action_access_scheduled_toot">โพสต์แบบตั้งเวลา</string>
</resources> </resources>

View File

@ -6,10 +6,10 @@
<string name="about_tusky_license">Husky özgür ve açık kaynak bir yazılımdır. GNU Genel Kamu Lisansı sürüm 3 altında lisanslanmıştır. Lisansı buradan görüntüleyebilirsiniz: https://www.gnu.org/licenses/gpl-3.0.en.html</string> <string name="about_tusky_license">Husky özgür ve açık kaynak bir yazılımdır. GNU Genel Kamu Lisansı sürüm 3 altında lisanslanmıştır. Lisansı buradan görüntüleyebilirsiniz: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_tusky_account">Husky\'nin Profili</string> <string name="about_tusky_account">Husky\'in Profili</string>
<string name="restart_emoji">Bu değişiklikleri uygulamak için Husky\'yi yeniden başlatmanız gerekecek</string> <string name="restart_emoji">Değişikliklerin uygulanabilmesi için uygulama yeniden başlatılmalı</string>
<string name="license_description">Husky aşağıdakıık kaynaklı projelerden kod ve materyal içeriyor:</string> <string name="license_description">Husky aşağıdakıık kaynaklı projelerden kod ve materyal içeriyor:</string>
@ -21,13 +21,13 @@
<string name="about_project_site">Projenin internet sitesi: <string name="about_project_site">Projenin internet sitesi:
\n https://huskyapp.dev</string> \n https://husky.fwgs.ru</string>
<string name="about_bug_feature_request_site">&amp; özellik istekleri hata raporları: <string name="about_bug_feature_request_site">Hata raporları &amp; özellik istekleri:
\n https://git.mentality.rip/FWGS/Husky/issues</string> \n https://git.mentality.rip/FWGS/Husky/issues</string>
@ -38,18 +38,15 @@
<string name="add_account_description">Yeni Pleroma hesabı ekle</string> <string name="add_account_description">Yeni Pleroma hesabı ekle</string>
<string name="warning_scheduling_interval">Pleroma\'un minimum 5 dakikalık zamanlama aralığı vardır.</string>
<string name="dialog_whats_an_instance">Burada her hangi bir Mastodon sunucusunun adresi (shitposter.club, blob.cat, expired.mentality.rip, ve <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">daha fazla!</a>) girilebiliri. <string name="dialog_whats_an_instance">Burada her hangi bir Mastodon sunucusunun adresi (shitposter.club, blob.cat, expired.mentality.rip, ve <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md">daha fazlasını</a>) girebilirsin!
\n \n
\nEğer hesabınız henüz yok ise katılmak istediğiniz sunucunun adresini girerek hesap yaratabilirsin. \nHenüz hesabın yok ise katılmak istediğin sunucunun adresini girerek hesap oluşturabilirsin
\n \n
\nHer bir sunucu hesaplar ağırlayan bir yer olur ancak diğer sunucularda bulunan insanlarla aynı sitede olmuşcasına iletişime geçip takip edebilirsiniz. \nHer bir sunucu kendi hesap kayıtlarını tutar ancak diğer sunucularda bulunan insanlarla aynı sitedeymişçesine iletişime geçip takip edebilirsin.
\n \n
\nDaha fazla bilgi için <a href="https://shitposter.club/about">shitposter.club</a>. </string> \nDaha fazla bilgi için <a href="https://shitposter.club/about">shitposter.club</a>. </string>

View File

@ -1,56 +0,0 @@
<resources>
<string name="about_tusky_license">Husky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Bạn có thể tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">Powered by Husky</string>
<string name="about_tusky_version">Husky %s</string>
<string name="about_tusky_account">Trang cá nhân Husky</string>
<string name="license_description">Husky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:</string>
<string name="restart_emoji">Bạn cần khởi động lại Husky để áp dụng các thiết lập</string>
<string name="about_project_site">Trang chủ
\nhttps://huskyapp.dev</string>
<string name="about_bug_feature_request_site">Báo lỗi và đề xuất tính năng
\nhttps://git.mentality.rip/FWGS/Husky/issues</string>
<string name="action_login">Đăng nhập Pleroma</string>
<string name="warning_scheduling_interval">Pleroma giới hạn tối thiểu 5 phút.</string>
<string name="add_account_description">Thêm tài khoản Pleroma</string>
<string name="dialog_whats_an_instance">Bạn phải nhập một tên miền, ví dụ shitposter.club, blob.cat, expired.mentality.rip, và <a href="https://fediverse.network/pleroma?count=peers">nhiều hơn nữa!</a>
\n
\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó.
\n
\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng.
\n
\nTham khảo thêm tại <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
</resources>

View File

@ -23,7 +23,7 @@
<string name="about_project_site"> <string name="about_project_site">
项目地址:\n 项目地址:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -43,18 +43,15 @@
<string name="add_account_description">添加新的 Pleroma 帐号</string> <string name="add_account_description">添加新的 Pleroma 帐号</string>
<string name="warning_scheduling_interval">Pleroma的最小预订时间为5分钟。</string> <string name="warning_scheduling_interval">Pleroma的最小调度间隔为5分钟。</string>
<string name="dialog_whats_an_instance">请输入你帐号所在的 Mastodon 站点的域名,比如 shitposter.clubblob.catexpired.mentality.rip<a href="https://fediverse.network/pleroma?count=peers">等等</a> <string name="dialog_whats_an_instance">请输入你帐号所在的 Mastodon 站点的域名,比如 pawoo.netacg.mnwxw.moe<a href="https://fediverse.network/pleroma?count=peers">等等</a>
\n \n\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Tusky 登入。
\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Tusky 登入。 \n\n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 <a href="https://joinmastodon.org">https://joinmastodon.org</a> 了解更多信息。
\n </string>
\n在 Mastodon 里,你的账号信息储存在某一特定实例当中,但 Mastodon 可使跨站互动和站内互动一样简单。
\n
\n可以前往 <a href="https://joinmastodon.org">https://joinmastodon.org</a> 了解更多信息。 </string>
</resources> </resources>

View File

@ -17,7 +17,7 @@
<string name="about_project_site"> <string name="about_project_site">
專案網站:\n 專案網站:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -39,7 +39,7 @@
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>
</resources> </resources>

View File

@ -17,7 +17,7 @@
<string name="about_project_site"> <string name="about_project_site">
專案網站:\n 專案網站:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -39,7 +39,7 @@
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>
</resources> </resources>

View File

@ -20,7 +20,7 @@
<string name="about_project_site"> <string name="about_project_site">
项目地址:\n 项目地址:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -20,7 +20,7 @@
<string name="about_project_site"> <string name="about_project_site">
專案網站:\n 專案網站:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>
@ -42,7 +42,7 @@
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>
</resources> </resources>

View File

@ -2,7 +2,7 @@
<!-- HUSKY SPECIFIC STRINGS --> <!-- HUSKY SPECIFIC STRINGS -->
<!-- REPLACEMENT FOR TUSKY STRINGS --> <!-- REPLACEMENT FOR TUSKY STRINGS -->
<string name="tusky_website" translatable="false">https://huskyapp.dev</string> <string name="tusky_website" translatable="false">https://husky.fwgs.ru</string>
</resources> </resources>

View File

@ -10,10 +10,4 @@
<item>@string/action_bbcode</item> <item>@string/action_bbcode</item>
<item>@string/action_html</item> <item>@string/action_html</item>
</string-array> </string-array>
<string name="description_muted_status" translatable="false">
<!-- Display name, relative date, username -->
%1$s; %2$s; %3$s
</string>
</resources> </resources>

View File

@ -25,7 +25,7 @@
<string name="about_project_site"> <string name="about_project_site">
Project website:\n Project website:\n
https://huskyapp.dev https://husky.fwgs.ru
</string> </string>

View File

@ -1,11 +1,5 @@
<resources> <resources>
<!-- HUSKY SPECIFIC STRINGS --> <!-- HUSKY SPECIFIC STRINGS -->
<string name="chats">Chats</string>
<string name="chat_our_last_message"><b>You</b></string>
<string name="error_chat_recipient_unavailable">Recipient does not support Chats</string>
<string name="action_mark_as_read">Mark as read</string>
<string name="action_reply_to">Reply to</string> <string name="action_reply_to">Reply to</string>
<string name="action_emoji_react">React</string> <string name="action_emoji_react">React</string>
<string name="action_emoji_unreact">Remove reaction</string> <string name="action_emoji_unreact">Remove reaction</string>
@ -13,10 +7,7 @@
<string name="action_enable_formatting_syntax">Enable %s</string> <string name="action_enable_formatting_syntax">Enable %s</string>
<string name="action_disable_formatting_syntax">Disable %s</string> <string name="action_disable_formatting_syntax">Disable %s</string>
<string name="action_sticker">Stickers</string> <string name="action_sticker">Stickers</string>
<string name="action_open_in_external_app">Open in external app</string>
<string name="action_chat">Open chat</string>
<string name="action_expand_menu">Expand menu</string>
<string name="title_emoji_reacted_by">%s reacted by</string> <string name="title_emoji_reacted_by">%s reacted by</string>
<string name="hint_appname">Application name</string> <string name="hint_appname">Application name</string>
@ -31,32 +22,12 @@
<string name="notification_emoji_format">%s reacted with %s to your post</string> <string name="notification_emoji_format">%s reacted with %s to your post</string>
<string name="notification_emoji_name">Emoji Reactions</string> <string name="notification_emoji_name">Emoji Reactions</string>
<string name="notification_emoji_description">Notifications about new emoji reactions</string> <string name="notification_emoji_description">Notifications about new emoji reactions</string>
<string name="notification_chat_message_format">%s sent you a message</string>
<string name="notification_chat_message_name">Chat Messages</string>
<string name="notification_chat_message_description">Notifications about new chat messages</string>
<string name="pref_title_other">Other</string>
<string name="pref_title_privacy">Privacy</string>
<string name="pref_title_anonymize_upload_filenames">Anonymize uploaded file names</string>
<string name="pref_title_live_notifications">Live notifications</string>
<string name="pref_summary_live_notifications">May slightly increase power consumption</string>
<string name="pref_title_default_formatting">Default formatting syntax(if supported by instance)</string> <string name="pref_title_default_formatting">Default formatting syntax(if supported by instance)</string>
<string name="pref_title_notification_filter_emoji">my posts are reacted with emojis</string> <string name="pref_title_notification_filter_emoji">my posts are reacted with emojis</string>
<string name="pref_title_notification_filter_chat_messages">received a chat message</string>
<string name="pref_title_hide_muted_users">Hide muted users</string> <string name="pref_title_hide_muted_users">Hide muted users</string>
<string name="pref_title_enable_big_emojis">Enable bigger custom emojis</string> <string name="pref_title_enable_big_emojis">Enable bigger custom emojis</string>
<string name="pref_title_enable_experimental_stickers">Enable experimental Pleroma-FE stickers(if available)</string> <string name="pref_title_enable_experimental_stickers">Enable experimental Pleroma-FE stickers(if available)</string>
<string name="attachment_type_image">Image</string>
<string name="attachment_type_video">Video</string>
<string name="attachment_type_audio">Audio</string>
<string name="attachment_type_unknown">Attachment</string>
<string name="link">Link</string> <!-- Web Link -->
<string name="streaming_notification_name">Live notifications</string>
<string name="streaming_notification_description">Running live notifications for: </string>
<!-- REPLACEMENT FOR TUSKY STRINGS --> <!-- REPLACEMENT FOR TUSKY STRINGS -->
<string name="action_toggle_visibility">Post visibility</string> <string name="action_toggle_visibility">Post visibility</string>
@ -109,12 +80,11 @@
<string name="status_share_content">Share content of post</string> <string name="status_share_content">Share content of post</string>
<string name="status_share_link">Share link to post</string> <string name="status_share_link">Share link to post</string>
<string name="status_boosted_format">%s repeated</string> <string name="status_boosted_format">%s repeated</string>
<string name="status_replied_to_format">Reply to %s</string>
<string name="title_scheduled_toot">Scheduled posts</string> <string name="title_scheduled_toot">Scheduled posts</string>
<string name="title_reblogged_by">Repeated by</string> <string name="title_reblogged_by">Repeated by</string>
<string name="title_view_thread">Post</string> <string name="title_view_thread">Post</string>
<!-- <!--
<string name="about_tusky_version">Husky %s</string> <string name="about_tusky_version">Husky %s</string>
<string name="about_powered_by_tusky">Powered by Husky</string> <string name="about_powered_by_tusky">Powered by Husky</string>

View File

@ -106,9 +106,6 @@
android:name=".components.compose.ComposeActivity" android:name=".components.compose.ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme" android:theme="@style/TuskyDialogActivityTheme"
android:windowSoftInputMode="stateVisible|adjustResize"/> android:windowSoftInputMode="stateVisible|adjustResize"/>
<activity
android:name=".components.chat.ChatActivity"
android:windowSoftInputMode="stateVisible|adjustResize"/>
<activity <activity
android:name=".ViewThreadActivity" android:name=".ViewThreadActivity"
android:configChanges="orientation|screenSize" /> android:configChanges="orientation|screenSize" />
@ -120,7 +117,7 @@
android:name=".AccountActivity" android:name=".AccountActivity"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
<activity android:name=".EditProfileActivity" /> <activity android:name=".EditProfileActivity" />
<activity android:name=".components.preference.PreferencesActivity" /> <activity android:name=".PreferencesActivity" />
<activity android:name=".StatusListActivity" /> <activity android:name=".StatusListActivity" />
<activity android:name=".AccountListActivity" /> <activity android:name=".AccountListActivity" />
<activity android:name=".AboutActivity" /> <activity android:name=".AboutActivity" />
@ -168,8 +165,6 @@
<service android:name=".service.SendTootService" /> <service android:name=".service.SendTootService" />
<service android:name=".service.StreamingService" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="ic_bbcode.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="32.222537"
inkscape:cy="16.813518"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-273)">
<g
aria-label="[ / ]"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;line-height:578.50018311;font-family:Roboto;-inkscape-font-specification:Roboto;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text821">
<path
d="m 5.4784346,278.1925 v 1.04731 H 4.1624082 v 11.5204 h 1.3160264 v 1.04731 H 2.8877229 V 278.1925 Z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path823" />
<path
d="M 14.532145,279.62566 10.3498,290.51905 H 9.2542592 l 4.1892358,-10.89339 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path825" />
<path
d="m 18.507785,279.23981 v -1.04731 h 2.604492 v 13.61502 h -2.604492 v -1.04731 h 1.322917 v -11.5204 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path827" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
sodipodi:docname="ic_html.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="29.06581"
inkscape:cy="16.813518"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-273)">
<g
aria-label="&lt;/&gt;"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:14.11111069px;line-height:578.50018311;font-family:Roboto;-inkscape-font-specification:Roboto;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
id="text821">
<path
d="m 7.9864639,288.23494 -5.636176,-2.61138 v -0.99908 l 5.636176,-2.60449 v 1.35048 l -4.299479,1.77078 4.299479,1.74321 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path888" />
<path
d="m 14.428792,279.5533 -4.182346,10.89339 h -1.09554 l 4.189236,-10.89339 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path890" />
<path
d="m 15.765489,282.00621 5.884223,2.60449 v 1.00597 l -5.884223,2.61138 v -1.31603 l 4.561306,-1.81212 -4.561306,-1.77766 z"
style="font-size:14.11111069px;line-height:578.50018311;stroke-width:0.26458332"
id="path892" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24mm"
height="24mm"
viewBox="0 0 24 24"
version="1.1"
id="svg8"
sodipodi:docname="ic_sticker.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="5.4548189"
inkscape:cy="71.290414"
inkscape:document-units="mm"
inkscape:current-layer="svg8"
showgrid="false"
inkscape:window-width="1876"
inkscape:window-height="1051"
inkscape:window-x="44"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:object-paths="true"
inkscape:object-nodes="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-global="true"
inkscape:snap-smooth-nodes="false"
inkscape:snap-midpoints="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
sodipodi:type="inkscape:offset"
inkscape:radius="0.61723572"
inkscape:original="M 17.623047 3.0683594 C 17.509421 3.0655494 17.393876 3.0680084 17.279297 3.078125 L 6.4902344 3.078125 C 4.5726672 3.0249708 2.9115899 4.8344018 3.0839844 6.7363281 C 3.0869044 10.385764 3.0795738 14.036202 3.0898438 17.685547 C 3.1383328 19.540878 4.9086202 21.084854 6.75 20.921875 L 11.855469 20.921875 L 12.105469 20.921875 L 12.355469 20.669922 L 20.644531 12.349609 L 20.927734 12.064453 L 20.927734 11.849609 C 20.927794 9.9997499 20.927719 8.1505896 20.923828 6.3007812 C 20.878728 4.5685513 19.327443 3.1105255 17.623047 3.0683594 z M 12.095703 4.0761719 C 13.93776 4.0746119 15.779621 4.0764408 17.621094 4.0917969 C 19.0166 4.1998379 20.08292 5.57683 19.927734 6.9511719 L 19.927734 11.849609 L 13.728516 11.849609 C 13.471455 11.849609 13.22575 11.902783 13.001953 11.998047 C 12.778155 12.093317 12.576116 12.230525 12.40625 12.400391 C 12.236384 12.570256 12.099172 12.772297 12.003906 12.996094 C 11.908636 13.219891 11.855469 13.465594 11.855469 13.722656 L 11.855469 19.921875 C 10.175931 19.921532 8.4977933 19.918927 6.8183594 19.908203 C 5.2022402 19.843685 3.9152358 18.271407 4.0839844 16.679688 C 4.0924326 13.222849 4.0677822 9.7667037 4.0976562 6.3105469 C 4.2045509 5.0752806 5.3270093 4.0566644 6.5683594 4.078125 C 8.4101806 4.082465 10.253646 4.0777473 12.095703 4.0761719 z M 14.162109 12.849609 L 15.355469 12.849609 L 18.732422 12.849609 L 12.855469 18.75 L 12.855469 15.349609 L 12.855469 14.15625 C 12.855469 13.977032 12.892568 13.804462 12.958984 13.648438 C 13.025404 13.492411 13.121807 13.352801 13.240234 13.234375 C 13.35866 13.115948 13.498271 13.019543 13.654297 12.953125 C 13.810323 12.886705 13.982892 12.849609 14.162109 12.849609 z "
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:40;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path4812"
d="m 17.638672,2.4511719 c -0.11531,-0.00285 -0.236952,0.00108 -0.359375,0.00977 H 6.5078125 C 4.1925376,2.396759 2.2610006,4.5009931 2.46875,6.7929688 l -0.00195,-0.056641 c 0.00292,3.6482799 -0.00442,7.2998419 0.00586,10.9511719 a 0.61729744,0.61729744 0 0 0 0,0.01367 c 0.058506,2.238617 2.1150294,4.032162 4.3320313,3.835937 L 6.75,21.539062 h 5.105469 0.25 a 0.61729744,0.61729744 0 0 0 0.4375,-0.18164 l 0.25,-0.251953 8.289062,-8.320313 L 21.365234,12.5 a 0.61729744,0.61729744 0 0 0 0.179688,-0.435547 v -0.214844 c 6e-5,-1.8498887 -1.4e-5,-3.7001239 -0.0039,-5.5507809 a 0.61729744,0.61729744 0 0 0 0,-0.013672 C 21.486566,4.1938021 19.685497,2.5018096 17.638672,2.4511719 Z m -5.542969,2.2421875 c 1.829572,-0.00155 3.657616,5.622e-4 5.484375,0.015625 1.014403,0.082688 1.848797,1.1605015 1.734375,2.1738281 a 0.61729744,0.61729744 0 0 0 -0.0039,0.068359 v 4.2812501 h -5.582031 c -0.34218,0 -0.671313,0.07066 -0.96875,0.197266 -0.297129,0.126486 -0.563672,0.309764 -0.789063,0.535156 -0.225391,0.225389 -0.40867,0.491924 -0.535156,0.789062 -0.126626,0.297454 -0.197266,0.626581 -0.197266,0.96875 v 5.580078 C 9.7727653,19.301874 8.3083381,19.300307 6.84375,19.291016 5.6139527,19.24192 4.5676965,17.9663 4.6972656,16.744141 a 0.61729744,0.61729744 0 0 0 0.00391,-0.0625 c 0.00842,-3.446433 -0.015574,-6.8868576 0.013672,-10.322266 0.079886,-0.892733 0.9478119,-1.6795516 1.84375,-1.6640625 a 0.61729744,0.61729744 0 0 0 0.00781,0 c 1.8438457,0.00434 3.6879418,-3.783e-4 5.5292968,-0.00195 z m 2.066406,8.7734376 h 1.19336 1.890625 l -3.773438,3.789062 v -1.90625 -1.193359 c 0,-0.09404 0.02007,-0.184299 0.05469,-0.265625 0.03409,-0.08008 0.08309,-0.155357 0.148437,-0.220703 0.06535,-0.06535 0.140636,-0.114354 0.220703,-0.148438 0.08132,-0.03462 0.171582,-0.05469 0.265625,-0.05469 z"
transform="translate(8.2803066e-4,-0.00103277)" />
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -15,8 +15,6 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator import android.animation.ArgbEvaluator
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -29,7 +27,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.Px import androidx.annotation.Px
@ -37,7 +34,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -52,7 +48,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.adapter.AccountFieldAdapter import com.keylesspalace.tusky.adapter.AccountFieldAdapter
import com.keylesspalace.tusky.components.chat.ChatActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
@ -66,11 +61,8 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_account.* import kotlinx.android.synthetic.main.activity_account.*
import kotlinx.android.synthetic.main.view_account_moved.* import kotlinx.android.synthetic.main.view_account_moved.*
import java.text.NumberFormat import java.text.NumberFormat
@ -168,6 +160,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountMuteButton.hide() accountMuteButton.hide()
accountFollowsYouTextView.hide() accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields // setup the RecyclerView for the account fields
accountFieldList.isNestedScrollingEnabled = false accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.layoutManager = LinearLayoutManager(this)
@ -286,7 +279,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFloatingActionButton.show() accountFloatingActionButton.show()
} }
if (verticalOffset < oldOffset) { if (verticalOffset < oldOffset) {
hideFabMenu()
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
} }
} }
@ -364,6 +356,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
swipeToRefreshLayout.isRefreshing = isRefreshing == true swipeToRefreshLayout.isRefreshing = isRefreshing == true
}) })
swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeToRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(this,
android.R.attr.colorBackground))
} }
private fun onAccountChanged(account: Account?) { private fun onAccountChanged(account: Account?) {
@ -502,57 +496,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} }
private fun FloatingActionButton.menuAnimate(show: Boolean) {
val height = this.height.toFloat()
if(show) {
visibility = View.VISIBLE
alpha = 0.0f
translationY = height
animate().setDuration(200)
.translationY(0.0f)
.alpha(1.0f)
.setListener(object : AnimatorListenerAdapter() {}) // seems listener is saved, so reset it here
.start()
} else {
animate().setDuration(200)
.translationY(height)
.alpha(0.0f)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
visibility = View.GONE
super.onAnimationEnd(animation)
}
})
.start()
}
}
private fun hideFabMenu() {
openedFabMenu = false
accountFloatingActionButton.animate().setDuration(200)
.rotation(0.0f).start()
accountFloatingActionButtonChat.menuAnimate(openedFabMenu)
accountFloatingActionButtonMention.menuAnimate(openedFabMenu)
}
var openedFabMenu = false
private fun animateFabMenu() {
if(openedFabMenu) {
hideFabMenu()
} else {
openedFabMenu = true
accountFloatingActionButton.animate().setDuration(200)
.rotation(135.0f).start()
accountFloatingActionButtonChat.menuAnimate(openedFabMenu)
accountFloatingActionButtonMention.menuAnimate(openedFabMenu)
}
}
/** /**
* Update account stat info * Update account stat info
*/ */
@ -563,28 +506,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFollowingTextView.text = numberFormat.format(account.followingCount) accountFollowingTextView.text = numberFormat.format(account.followingCount)
accountStatusesTextView.text = numberFormat.format(account.statusesCount) accountStatusesTextView.text = numberFormat.format(account.statusesCount)
accountFloatingActionButtonMention.setOnClickListener { mention() } accountFloatingActionButton.setOnClickListener { mention() }
if(account.pleroma?.acceptsChatMessages == true) {
accountFloatingActionButtonChat.setOnClickListener {
mastodonApi.createChat(account.id)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({
val intent = ChatActivity.getIntent(this@AccountActivity, it)
startActivityWithSlideInAnimation(intent)
}, {
Toast.makeText(this@AccountActivity, getString(R.string.error_generic), Toast.LENGTH_SHORT).show()
})
}
} else {
accountFloatingActionButtonChat.backgroundTintList = ColorStateList.valueOf(Color.GRAY)
accountFloatingActionButtonChat.setOnClickListener {
Toast.makeText(this@AccountActivity, getString(R.string.error_chat_recipient_unavailable), Toast.LENGTH_SHORT).show()
}
}
accountFloatingActionButton.setOnClickListener { animateFabMenu() }
accountFollowButton.setOnClickListener { accountFollowButton.setOnClickListener {
if (viewModel.isSelf) { if (viewModel.isSelf) {
@ -691,7 +613,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
updateFollowButton() updateFollowButton()
if (blocking || viewModel.isSelf) { if (blocking || viewModel.isSelf) {
hideFabMenu()
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountMuteButton.hide() accountMuteButton.hide()
accountSubscribeButton.hide() accountSubscribeButton.hide()
@ -705,7 +626,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} else { } else {
hideFabMenu()
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountFollowButton.hide() accountFollowButton.hide()
accountMuteButton.hide() accountMuteButton.hide()
@ -909,10 +829,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} else null } else null
} }
override fun onActionButtonHidden() {
hideFabMenu()
}
override fun androidInjector() = dispatchingAndroidInjector override fun androidInjector() = dispatchingAndroidInjector
companion object { companion object {

View File

@ -23,8 +23,6 @@ import android.widget.Toast
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.components.chat.ChatActivity
import com.keylesspalace.tusky.entity.Chat
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.LinkHelper
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
@ -114,10 +112,6 @@ abstract class BottomSheetActivity : BaseActivity() {
startActivityWithSlideInAnimation(intent) startActivityWithSlideInAnimation(intent)
} }
open fun openChat(chat: Chat) {
startActivityWithSlideInAnimation(ChatActivity.getIntent(this, chat))
}
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) { protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
when (fallbackBehavior) { when (fallbackBehavior) {
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url) PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)

View File

@ -30,7 +30,6 @@ import android.widget.ImageView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -113,7 +112,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
addFieldButton.setOnClickListener { addFieldButton.setOnClickListener {
accountFieldEditAdapter.addField() accountFieldEditAdapter.addField()
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
it.isVisible = false it.isEnabled = false
} }
scrollView.post{ scrollView.post{

View File

@ -0,0 +1,291 @@
package com.keylesspalace.tusky;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.util.EmojiCompatFont;
import java.util.ArrayList;
/**
* This Preference lets the user select their preferred emoji font
*/
public class EmojiPreference extends Preference {
private static final String TAG = "EmojiPreference";
private EmojiCompatFont selected, original;
static final String FONT_PREFERENCE = "selected_emoji_font";
private static final EmojiCompatFont[] FONTS = EmojiCompatFont.FONTS;
// Please note that this array should be sorted in the same way as their fonts.
private static final int[] viewIds = {
R.id.item_nomoji,
R.id.item_blobmoji,
R.id.item_twemoji,
R.id.item_notoemoji};
private ArrayList<RadioButton> radioButtons = new ArrayList<>();
private boolean updated, currentNeedsUpdate;
public EmojiPreference(Context context) {
super(context);
// Find out which font is currently active
this.selected = EmojiCompatFont.byId(PreferenceManager
.getDefaultSharedPreferences(context)
.getInt(FONT_PREFERENCE, 0));
// We'll use this later to determine if anything has changed
this.original = this.selected;
setSummary(selected.getDisplay(context));
}
public EmojiPreference(Context context, AttributeSet attrs) {
super(context, attrs);
// Find out which font is currently active
this.selected = EmojiCompatFont.byId(PreferenceManager
.getDefaultSharedPreferences(context)
.getInt(FONT_PREFERENCE, 0));
// We'll use this later to determine if anything has changed
this.original = this.selected;
setSummary(selected.getDisplay(context));
}
@Override
protected void onClick() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_emojicompat, null);
for (int i = 0; i < viewIds.length; i++) {
setupItem(view.findViewById(viewIds[i]), FONTS[i]);
}
new AlertDialog.Builder(getContext())
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog, which) -> onDialogOk())
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void setupItem(View container, EmojiCompatFont font) {
Context context = container.getContext();
TextView title = container.findViewById(R.id.emojicompat_name);
TextView caption = container.findViewById(R.id.emojicompat_caption);
ImageView thumb = container.findViewById(R.id.emojicompat_thumb);
ImageButton download = container.findViewById(R.id.emojicompat_download);
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
RadioButton radio = container.findViewById(R.id.emojicompat_radio);
// Initialize all the views
title.setText(font.getDisplay(context));
caption.setText(font.getCaption(context));
thumb.setImageDrawable(font.getThumb(context));
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
radioButtons.add(radio);
updateItem(font, container);
// Set actions
download.setOnClickListener((downloadButton) ->
startDownload(font, container));
cancel.setOnClickListener((cancelButton) ->
cancelDownload(font, container));
radio.setOnClickListener((radioButton) ->
select(font, (RadioButton) radioButton));
container.setOnClickListener((containterView) ->
select(font,
containterView.findViewById(R.id.emojicompat_radio
)));
}
private void startDownload(EmojiCompatFont font, View container) {
ImageButton download = container.findViewById(R.id.emojicompat_download);
TextView caption = container.findViewById(R.id.emojicompat_caption);
ProgressBar progressBar = container.findViewById(R.id.emojicompat_progress);
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
// Switch to downloading style
download.setVisibility(View.GONE);
caption.setVisibility(View.INVISIBLE);
progressBar.setVisibility(View.VISIBLE);
cancel.setVisibility(View.VISIBLE);
font.downloadFont(getContext(), new EmojiCompatFont.Downloader.EmojiDownloadListener() {
@Override
public void onDownloaded(EmojiCompatFont font) {
finishDownload(font, container);
}
@Override
public void onProgress(float progress) {
// The progress is returned as a float between 0 and 1
progress *= progressBar.getMax();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress((int) progress, true);
} else {
progressBar.setProgress((int) progress);
}
}
@Override
public void onFailed() {
Toast.makeText(getContext(), R.string.download_failed, Toast.LENGTH_SHORT).show();
updateItem(font, container);
}
});
}
private void cancelDownload(EmojiCompatFont font, View container) {
font.cancelDownload();
updateItem(font, container);
}
private void finishDownload(EmojiCompatFont font, View container) {
select(font, container.findViewById(R.id.emojicompat_radio));
updateItem(font, container);
// Set the flag to restart the app (because an update has been downloaded)
if (selected == original && currentNeedsUpdate) {
updated = true;
currentNeedsUpdate = false;
}
}
/**
* Select a font both visually and logically
*
* @param font The font to be selected
* @param radio The radio button associated with it's visual item
*/
private void select(EmojiCompatFont font, RadioButton radio) {
selected = font;
// Uncheck all the other buttons
for (RadioButton other : radioButtons) {
if (other != radio) {
other.setChecked(false);
}
}
radio.setChecked(true);
}
/**
* Called when a "consistent" state is reached, i.e. it's not downloading the font
*
* @param font The font to be displayed
* @param container The ConstraintLayout containing the item
*/
private void updateItem(EmojiCompatFont font, View container) {
// Assignments
ImageButton download = container.findViewById(R.id.emojicompat_download);
TextView caption = container.findViewById(R.id.emojicompat_caption);
ProgressBar progress = container.findViewById(R.id.emojicompat_progress);
ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
RadioButton radio = container.findViewById(R.id.emojicompat_radio);
// There's no download going on
progress.setVisibility(View.GONE);
cancel.setVisibility(View.GONE);
caption.setVisibility(View.VISIBLE);
if (font.isDownloaded(getContext())) {
// Make it selectable
download.setVisibility(View.GONE);
radio.setVisibility(View.VISIBLE);
container.setClickable(true);
} else {
// Make it downloadable
download.setVisibility(View.VISIBLE);
radio.setVisibility(View.GONE);
container.setClickable(false);
}
// Select it if necessary
if (font == selected) {
radio.setChecked(true);
// Update available
if (!font.isDownloaded(getContext())) {
currentNeedsUpdate = true;
}
} else {
radio.setChecked(false);
}
}
/**
* In order to be able to use this font later on, it needs to be saved first.
*/
private void saveSelectedFont() {
int index = selected.getId();
Log.i(TAG, "saveSelectedFont: Font ID: " + index);
// It's saved using the key FONT_PREFERENCE
PreferenceManager
.getDefaultSharedPreferences(getContext())
.edit()
.putInt(FONT_PREFERENCE, index)
.apply();
setSummary(selected.getDisplay(getContext()));
}
/**
* That's it. The user doesn't want to switch between these amazing radio buttons anymore!
* That means, the selected font can be saved (if the user hit OK)
*/
private void onDialogOk() {
saveSelectedFont();
if (selected != original || updated) {
new AlertDialog.Builder(getContext())
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart, ((dialog, which) -> {
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
Intent launchIntent = new Intent(getContext(), SplashActivity.class);
PendingIntent mPendingIntent = PendingIntent.getActivity(
getContext(),
// This is the codepoint of the party face emoji :D
0x1f973,
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager mgr =
(AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
if (mgr != null) {
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent);
}
System.exit(0);
})).show();
}
}
}

View File

@ -19,6 +19,7 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
@ -28,6 +29,7 @@ import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -38,10 +40,6 @@ import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.FixedSizeDrawable
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
@ -52,19 +50,15 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.service.StreamingService
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -80,10 +74,6 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
@ -100,12 +90,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
lateinit var conversationRepository: ConversationsRepository lateinit var conversationRepository: ConversationsRepository
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private lateinit var drawerToggle: ActionBarDrawerToggle
private var notificationTabPosition = 0 private var notificationTabPosition = 0
private var onTabSelectedListener: OnTabSelectedListener? = null private var onTabSelectedListener: OnTabSelectedListener? = null
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
private val emojiInitCallback = object : InitCallback() { private val emojiInitCallback = object : InitCallback() {
override fun onInitialized() { override fun onInitialized() {
if (!isDestroyed) { if (!isDestroyed) {
@ -175,12 +164,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivity(composeIntent) startActivity(composeIntent)
} }
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
mainToolbar.visible(!hideTopToolbar)
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
mainToolbar.navigationIcon = FixedSizeDrawable(getDrawable(R.drawable.avatar_default), navIconSize, navIconSize)
mainToolbar.menu.add(R.string.action_search).apply { mainToolbar.menu.add(R.string.action_search).apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
@ -193,7 +176,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
} }
setupDrawer(savedInstanceState, addSearchButton = hideTopToolbar) setupDrawer(savedInstanceState)
/* Fetch user info while we're doing other things. This has to be done after setting up the /* Fetch user info while we're doing other things. This has to be done after setting up the
* drawer, though, because its callback touches the header in the drawer. */ * drawer, though, because its callback touches the header in the drawer. */
@ -201,6 +184,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupTabs(showNotificationTab) setupTabs(showNotificationTab)
// Setup push notifications
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
NotificationHelper.enablePullNotifications(this)
} else {
NotificationHelper.disablePullNotifications(this)
}
eventHub.events eventHub.events
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY) .autoDispose(this, Lifecycle.Event.ON_DESTROY)
@ -208,13 +197,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
when (event) { when (event) {
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
is MainTabsChangedEvent -> setupTabs(false) is MainTabsChangedEvent -> setupTabs(false)
is PreferenceChangedEvent -> {
when(event.preferenceKey) {
PrefKeys.LIVE_NOTIFICATIONS -> {
initPullNotifications()
}
}
}
} }
} }
@ -222,21 +204,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Husky")) deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Husky"))
} }
private fun initPullNotifications() {
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
if(accountManager.areNotificationsStreamingEnabled()) {
StreamingService.startStreaming(this)
NotificationHelper.disablePullNotifications(this)
} else {
StreamingService.stopStreaming(this)
NotificationHelper.enablePullNotifications(this)
}
} else {
StreamingService.stopStreaming(this)
NotificationHelper.disablePullNotifications(this)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager) NotificationHelper.clearNotificationsForActiveAccount(this, accountManager)
@ -289,6 +256,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
public override fun onPostCreate(savedInstanceState: Bundle?) { public override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
drawerToggle.syncState()
if (intent != null) { if (intent != null) {
val statusUrl = intent.getStringExtra(STATUS_URL) val statusUrl = intent.getStringExtra(STATUS_URL)
if (statusUrl != null) { if (statusUrl != null) {
@ -312,11 +281,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
finish() finish()
} }
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { private fun setupDrawer(savedInstanceState: Bundle?) {
mainToolbar.setNavigationOnClickListener { drawerToggle = ActionBarDrawerToggle(this, mainDrawerLayout, mainToolbar, com.mikepenz.materialdrawer.R.string.material_drawer_open, com.mikepenz.materialdrawer.R.string.material_drawer_close)
mainDrawerLayout.open()
}
header = AccountHeaderView(this).apply { header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@ -335,7 +302,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.accountHeaderBackground.setColorFilter(ContextCompat.getColor(this, R.color.headerBackgroundFilter)) header.accountHeaderBackground.setColorFilter(ContextCompat.getColor(this, R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent)) header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent))
val animateAvatars = preferences.getBoolean("animateGifAvatars", false) val animateAvatars = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean("animateGifAvatars", false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
@ -447,18 +415,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
onClick = ::logout onClick = ::logout
} }
) )
if (addSearchButton) {
mainDrawer.addItemsAtPosition(4,
primaryDrawerItem {
nameRes = R.string.action_search
iconicsIcon = GoogleMaterial.Icon.gmd_search
onClick = {
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
}
})
}
setSavedInstance(savedInstanceState) setSavedInstance(savedInstanceState)
} }
@ -474,13 +430,26 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
EmojiCompat.get().registerInitCallback(emojiInitCallback) EmojiCompat.get().registerInitCallback(emojiInitCallback)
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
drawerToggle.onConfigurationChanged(newConfig)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (drawerToggle.onOptionsItemSelected(item)) {
return true
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) super.onSaveInstanceState(mainDrawer.saveInstanceState(outState))
} }
private fun setupTabs(selectNotificationTab: Boolean) { private fun setupTabs(selectNotificationTab: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val activeTabLayout = if(preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin (composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
@ -599,7 +568,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
conversationRepository.deleteCacheForAccount(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id)
removeShortcut(this, activeAccount) removeShortcut(this, activeAccount)
val newAccount = accountManager.logActiveAccountOut() val newAccount = accountManager.logActiveAccountOut()
initPullNotifications() if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
NotificationHelper.disablePullNotifications(this)
}
val intent = if (newAccount == null) { val intent = if (newAccount == null) {
LoginActivity.getIntent(this, false) LoginActivity.getIntent(this, false)
} else { } else {
@ -633,30 +604,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.load(me.header) .load(me.header)
.into(header.accountHeaderBackground) .into(header.accountHeaderBackground)
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
Glide.with(this)
.asDrawable()
.override(navIconSize)
.load(me.avatar)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
mainToolbar.navigationIcon = resource
}
override fun onLoadCleared(placeholder: Drawable?) {
mainToolbar.navigationIcon = placeholder
}
})
accountManager.updateActiveAccount(me) accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
initPullNotifications()
// Show follow requests in the menu, if this is a locked account. // Show follow requests in the menu, if this is a locked account.
if (me.locked && mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) { if (me.locked && mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) {
val followRequestsItem = primaryDrawerItem { val followRequestsItem = primaryDrawerItem {

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -23,12 +23,9 @@ import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.fragment.preference.*
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
@ -132,8 +129,8 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "hideMutedUsers", "useBlurhash", "showCardsInTimelines", "confirmReblogs", "hideMutedUsers",
"enableSwipeForTabs", "bigEmojis", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> { "enableSwipeForTabs", "bigEmojis", "mainNavPosition" -> {
restartActivitiesOnExit = true restartActivitiesOnExit = true
} }
"language" -> { "language" -> {

View File

@ -179,8 +179,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
/*scheduledAt*/null, /*scheduledAt*/null,
/*sensitive*/null, /*sensitive*/null,
/*poll*/null, /*poll*/null,
item.getFormattingSyntax(), item.getFormattingSyntax()
/* modifiedInitialState */ true
); );
Intent intent = ComposeActivity.startIntent(this, composeOptions); Intent intent = ComposeActivity.startIntent(this, composeOptions);
startActivity(intent); startActivity(intent);

View File

@ -20,7 +20,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.fragment.ChatsFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
@ -33,7 +32,6 @@ const val FEDERATED = "Federated"
const val DIRECT = "Direct" const val DIRECT = "Direct"
const val HASHTAG = "Hashtag" const val HASHTAG = "Hashtag"
const val LIST = "List" const val LIST = "List"
const val CHATS = "Chats"
data class TabData(val id: String, data class TabData(val id: String,
@StringRes val text: Int, @StringRes val text: Int,
@ -91,12 +89,6 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
arguments, arguments,
{ arguments.getOrNull(1).orEmpty() } { arguments.getOrNull(1).orEmpty() }
) )
CHATS -> TabData(
CHATS,
R.string.chats,
R.drawable.ic_forum_24px,
{ ChatsFragment() }
)
else -> throw IllegalArgumentException("unknown tab type") else -> throw IllegalArgumentException("unknown tab type")
} }
} }
@ -106,7 +98,6 @@ fun defaultTabs(): List<TabData> {
createTabDataFromId(HOME), createTabDataFromId(HOME),
createTabDataFromId(NOTIFICATIONS), createTabDataFromId(NOTIFICATIONS),
createTabDataFromId(LOCAL), createTabDataFromId(LOCAL),
createTabDataFromId(FEDERATED), createTabDataFromId(FEDERATED)
createTabDataFromId(CHATS)
) )
} }

View File

@ -78,7 +78,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList()
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
currentTabsRecyclerView.adapter = currentTabsAdapter currentTabsRecyclerView.adapter = currentTabsAdapter
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
@ -175,20 +175,20 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
saveTabs() saveTabs()
} }
override fun onActionChipClicked(tab: TabData, tabPosition: Int) { override fun onActionChipClicked(tab: TabData) {
showAddHashtagDialog(tab, tabPosition) showAddHashtagDialog(tab)
} }
override fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) { override fun onChipClicked(tab: TabData, chipPosition: Int) {
val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition } val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition }
val newTab = tab.copy(arguments = newArguments) val newTab = tab.copy(arguments = newArguments)
currentTabs[tabPosition] = newTab val position = currentTabs.indexOf(tab)
saveTabs() currentTabs[position] = newTab
currentTabsAdapter.notifyItemChanged(tabPosition) currentTabsAdapter.notifyItemChanged(position)
} }
private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { private fun showAddHashtagDialog(tab: TabData? = null) {
val frameLayout = FrameLayout(this) val frameLayout = FrameLayout(this)
val padding = Utils.dpToPx(this, 8) val padding = Utils.dpToPx(this, 8)
@ -211,9 +211,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else { } else {
val newTab = tab.copy(arguments = tab.arguments + input) val newTab = tab.copy(arguments = tab.arguments + input)
currentTabs[tabPosition] = newTab val position = currentTabs.indexOf(tab)
currentTabs[position] = newTab
currentTabsAdapter.notifyItemChanged(tabPosition) currentTabsAdapter.notifyItemChanged(position)
} }
updateAvailableTabs() updateAvailableTabs()
@ -285,10 +286,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
if (!currentTabs.contains(directMessagesTab)) { if (!currentTabs.contains(directMessagesTab)) {
addableTabs.add(directMessagesTab) addableTabs.add(directMessagesTab)
} }
val chatTab = createTabDataFromId(CHATS)
if (!currentTabs.contains(chatTab)) {
addableTabs.add(chatTab)
}
addableTabs.add(createTabDataFromId(HASHTAG)) addableTabs.add(createTabDataFromId(HASHTAG))
addableTabs.add(createTabDataFromId(LIST)) addableTabs.add(createTabDataFromId(LIST))
@ -346,7 +343,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
companion object { companion object {
private const val MIN_TAB_COUNT = 2 private const val MIN_TAB_COUNT = 2
private const val MAX_TAB_COUNT = 9 private const val MAX_TAB_COUNT = 5
} }
} }

View File

@ -18,17 +18,14 @@ package com.keylesspalace.tusky
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.util.Log import android.util.Log
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkManager import androidx.work.WorkManager
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideCustomImageLoader import com.github.piasy.biv.loader.glide.GlideCustomImageLoader
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.uber.autodispose.AutoDisposePlugins import com.uber.autodispose.AutoDisposePlugins
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
@ -58,7 +55,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
// init the custom emoji fonts // init the custom emoji fonts
val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection) val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this) .getConfig(this)
.setReplaceAll(true) .setReplaceAll(true)
@ -79,7 +76,6 @@ class TuskyApplication : Application(), HasAndroidInjector {
Log.w("RxJava", "undeliverable exception", it) Log.w("RxJava", "undeliverable exception", it)
} }
SubsamplingScaleImageView.setPreferredBitmapConfig(Bitmap.Config.ARGB_8888)
BigImageViewer.initialize(GlideCustomImageLoader.with(this)) BigImageViewer.initialize(GlideCustomImageLoader.with(this))
} }

View File

@ -1,227 +0,0 @@
package com.keylesspalace.tusky.adapter
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.text.TextUtils
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.interfaces.ChatActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.view.MediaPreviewImageView
import com.keylesspalace.tusky.viewdata.ChatMessageViewData
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
object Key {
const val KEY_CREATED = "created"
}
private val content: TextView = view.findViewById(R.id.content)
private val timestamp: TextView = view.findViewById(R.id.datetime)
private val attachmentView: MediaPreviewImageView = view.findViewById(R.id.attachment)
private val mediaOverlay: ImageView = view.findViewById(R.id.mediaOverlay)
private val attachmentLayout: FrameLayout = view.findViewById(R.id.attachmentLayout)
private val sdf = SimpleDateFormat("HH:mm", Locale.getDefault())
private val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(itemView.context, R.attr.colorBackgroundAccent))
fun setupWithChatMessage(msg: ChatMessageViewData.Concrete, chatActionListener: ChatActionListener, statusDisplayOptions: StatusDisplayOptions, payload: Any?) {
if(payload == null) {
if(msg.content != null)
content.text = msg.content.emojify(msg.emojis, content)
setAttachment(msg.attachment, chatActionListener)
setCreatedAt(msg.createdAt)
} else {
if(payload is List<*>) {
for (item in payload) {
if (ChatsViewHolder.Key.KEY_CREATED == item) {
setCreatedAt(msg.createdAt)
}
}
}
}
}
private fun loadImage(imageView: MediaPreviewImageView,
previewUrl: String?,
meta: Attachment.MetaData?) {
if (TextUtils.isEmpty(previewUrl)) {
imageView.removeFocalPoint()
Glide.with(imageView)
.load(mediaPreviewUnloaded)
.centerInside()
.into(imageView)
} else {
val focus = meta?.focus
if (focus != null) { // If there is a focal point for this attachment:
imageView.setFocalPoint(focus)
Glide.with(imageView)
.load(previewUrl)
.placeholder(mediaPreviewUnloaded)
.centerInside()
.addListener(imageView)
.into(imageView)
} else {
imageView.removeFocalPoint()
Glide.with(imageView)
.load(previewUrl)
.placeholder(mediaPreviewUnloaded)
.centerInside()
.into(imageView)
}
}
}
private fun formatDuration(durationInSeconds: Double): String? {
val seconds = durationInSeconds.roundToInt().toInt() % 60
val minutes = durationInSeconds.toInt() % 3600 / 60
val hours = durationInSeconds.toInt() / 3600
return String.format("%d:%02d:%02d", hours, minutes, seconds)
}
private fun getAttachmentDescription(context: Context, attachment: Attachment): CharSequence {
var duration = ""
if (attachment.meta?.duration != null && attachment.meta.duration > 0) {
duration = formatDuration(attachment.meta.duration.toDouble()) + " "
}
return if (TextUtils.isEmpty(attachment.description)) {
duration + context.getString(R.string.description_status_media_no_description_placeholder)
} else {
duration + attachment.description
}
}
private fun setAttachmentClickListener(view: View, listener: ChatActionListener, attachment: Attachment, animateTransition: Boolean) {
view.setOnClickListener { v: View? ->
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onViewMedia(position, if (animateTransition) v else null)
}
}
view.setOnLongClickListener { v: View? ->
val description = getAttachmentDescription(view.context, attachment)
Toast.makeText(view.context, description, Toast.LENGTH_LONG).show()
true
}
}
private fun setAttachment(attachment: Attachment?, listener: ChatActionListener) {
if(attachment == null) {
attachmentLayout.visibility = View.GONE
} else {
attachmentLayout.visibility = View.VISIBLE
val previewUrl: String = attachment.previewUrl
val description: String? = attachment.description
if(description != null && TextUtils.isEmpty(description) ) {
attachmentView.contentDescription = description
} else {
attachmentView.contentDescription = attachmentView.context
.getString(R.string.action_view_media)
}
loadImage(attachmentView, previewUrl, attachment.meta)
when(attachment.type) {
Attachment.Type.VIDEO, Attachment.Type.GIFV -> {
mediaOverlay.visibility = View.VISIBLE
}
else -> {
mediaOverlay.visibility = View.GONE
}
}
setAttachmentClickListener(attachmentView, listener, attachment, true)
}
}
private fun setCreatedAt(createdAt: Date) {
timestamp.text = sdf.format(createdAt)
}
}
class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource<ChatMessageViewData>,
private val chatActionListener: ChatActionListener,
private val statusDisplayOptions: StatusDisplayOptions,
private val localUserId: String)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_OUR_MESSAGE = 0
private val VIEW_TYPE_THEIR_MESSAGE = 1
private val VIEW_TYPE_PLACEHOLDER = 2
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when(viewType) {
VIEW_TYPE_OUR_MESSAGE -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_our_message, parent, false)
return ChatMessagesViewHolder(view)
}
VIEW_TYPE_THEIR_MESSAGE -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_their_message, parent, false)
return ChatMessagesViewHolder(view)
}
else -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status_placeholder, parent, false)
return PlaceholderViewHolder(view)
}
}
}
override fun getItemCount(): Int {
return dataSource.itemCount
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
bindViewHolder(holder, position, null)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList<Any>) {
bindViewHolder(holder, position, payload)
}
private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>?) {
val chat: ChatMessageViewData = dataSource.getItemAt(position)
if(holder is PlaceholderViewHolder) {
holder.setup(chatActionListener, (chat as ChatMessageViewData.Placeholder).isLoading)
} else if(holder is ChatMessagesViewHolder) {
holder.setupWithChatMessage(chat as ChatMessageViewData.Concrete, chatActionListener, statusDisplayOptions,
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null)
}
}
override fun getItemViewType(position: Int): Int {
if(dataSource.getItemAt(position) is ChatMessageViewData.Concrete) {
val msg = dataSource.getItemAt(position) as ChatMessageViewData.Concrete
if(msg.accountId == localUserId) {
return VIEW_TYPE_OUR_MESSAGE
}
return VIEW_TYPE_THEIR_MESSAGE
}
return VIEW_TYPE_PLACEHOLDER
}
override fun getItemId(position: Int): Long {
return dataSource.getItemAt(position).getViewDataId().toLong()
}
}

View File

@ -1,209 +0,0 @@
package com.keylesspalace.tusky.adapter
import android.graphics.Typeface
import android.opengl.Visibility
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.text.toSpanned
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.interfaces.ChatActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.viewdata.ChatViewData
import java.text.SimpleDateFormat
import java.util.*
class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
object Key {
const val KEY_CREATED = "created"
}
private val avatar: ImageView = view.findViewById(R.id.status_avatar)
private val avatarInset: ImageView = view.findViewById(R.id.status_avatar_inset)
private val displayName: TextView = view.findViewById(R.id.status_display_name)
private val userName: TextView = view.findViewById(R.id.status_username)
private val timestamp: TextView = view.findViewById(R.id.status_timestamp_info)
private val content: TextView = view.findViewById(R.id.status_content)
private val unread: TextView = view.findViewById(R.id.chat_unread)
private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
fun setupWithChat(chat: ChatViewData.Concrete,
listener: ChatActionListener,
statusDisplayOptions: StatusDisplayOptions,
localUserId: String,
payload: Any?) {
if (payload == null) {
displayName.text = chat.account.displayName?.emojify(chat.account.emojis, displayName, true)
?: ""
userName.text = userName.context.getString(R.string.status_username_format, chat.account.username)
setUpdatedAt(chat.updatedAt, statusDisplayOptions)
setAvatar(chat.account.avatar, chat.account.bot, statusDisplayOptions)
if (chat.unread <= 0) {
unread.visibility = View.GONE
} else if (chat.unread > 99) {
unread.text = ":)"
} else {
unread.text = chat.unread.toString()
}
avatar.setOnClickListener { listener.onViewAccount(chat.account.id) }
val onLongClickListener = View.OnLongClickListener {
listener.onMore(chat.id, it)
true
}
val onClickListener = View.OnClickListener {
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION)
listener.openChat(pos)
}
content.setOnLongClickListener(onLongClickListener)
itemView.setOnLongClickListener(onLongClickListener)
content.setOnClickListener(onClickListener)
itemView.setOnClickListener(onClickListener)
if(chat.lastMessage != null) {
var text = if (chat.lastMessage.content != null) {
content.setTypeface(null, Typeface.NORMAL)
chat.lastMessage.content.emojify(chat.lastMessage.emojis, content, true)
} else if (chat.lastMessage.attachment != null) {
content.setTypeface(null, Typeface.ITALIC)
content.resources.getString(chat.lastMessage.attachment.describeAttachmentType())
} else if (chat.lastMessage.card != null) {
content.setTypeface(null, Typeface.ITALIC)
content.resources.getString(R.string.link)
} else ""
content.text = if(chat.lastMessage.accountId == localUserId) {
SpannableStringBuilder.valueOf(content.resources.getText(R.string.chat_our_last_message))
.append(": $text")
} else text
} else {
content.text = ""
}
} else {
if(payload is List<*>) {
for (item in payload as List<*>) {
if (Key.KEY_CREATED == item) {
setUpdatedAt(chat.updatedAt, statusDisplayOptions)
}
}
}
}
}
private fun setAvatar(url: String,
isBot: Boolean,
statusDisplayOptions: StatusDisplayOptions) {
avatar.setPaddingRelative(0, 0, 0, 0)
if (statusDisplayOptions.showBotOverlay && isBot) {
avatarInset.visibility = View.VISIBLE
avatarInset.setBackgroundColor(0x50ffffff)
Glide.with(avatarInset)
.load(R.drawable.ic_bot_24dp)
.into(avatarInset)
} else {
avatarInset.visibility = View.GONE
}
val avatarRadius = itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
loadAvatar(url, avatar, avatarRadius,
statusDisplayOptions.animateAvatars)
}
private fun getAbsoluteTime(createdAt: Date?): String? {
if (createdAt == null) {
return "??:??:??"
}
return if (DateUtils.isToday(createdAt.time)) {
shortSdf.format(createdAt)
} else {
longSdf.format(createdAt)
}
}
private fun setUpdatedAt(updatedAt: Date, statusDisplayOptions: StatusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime) {
timestamp.text = getAbsoluteTime(updatedAt)
} else {
val then = updatedAt.time
val now = System.currentTimeMillis()
val readout = TimestampUtils.getRelativeTimeSpanString(timestamp.context, then, now)
timestamp.text = readout
}
}
}
class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource<ChatViewData>,
val statusDisplayOptions: StatusDisplayOptions,
private val chatActionListener: ChatActionListener,
val localUserId: String) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_CHAT = 0
private val VIEW_TYPE_PLACEHOLDER = 1
override fun getItemCount(): Int {
return dataSource.itemCount
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
bindViewHolder(holder, position, null)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList<Any>) {
bindViewHolder(holder, position, payload)
}
private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>?) {
val chat: ChatViewData = dataSource.getItemAt(position)
if(holder is PlaceholderViewHolder) {
holder.setup(chatActionListener, (chat as ChatViewData.Placeholder).isLoading)
} else if(holder is ChatsViewHolder) {
holder.setupWithChat(chat as ChatViewData.Concrete, chatActionListener,
statusDisplayOptions, localUserId,
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if(viewType == VIEW_TYPE_CHAT ) {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_chat, parent, false)
return ChatsViewHolder(view)
}
// else VIEW_TYPE_PLACEHOLDER
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status_placeholder, parent, false)
return PlaceholderViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
if(dataSource.getItemAt(position) is ChatViewData.Concrete)
return VIEW_TYPE_CHAT
return VIEW_TYPE_PLACEHOLDER
}
override fun getItemId(position: Int): Long {
return dataSource.getItemAt(position).getViewDataId().toLong()
}
}

View File

@ -21,7 +21,6 @@ import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.ChatActionListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
@ -35,26 +34,16 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
progressBar = itemView.findViewById(R.id.progressBar); progressBar = itemView.findViewById(R.id.progressBar);
} }
private void setup(boolean progress) { public void setup(final StatusActionListener listener, boolean progress) {
loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE); loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE);
progressBar.setVisibility(progress ? View.VISIBLE : View.GONE); progressBar.setVisibility(progress ? View.VISIBLE : View.GONE);
loadMoreButton.setEnabled(true); loadMoreButton.setEnabled(true);
}
public void setup(final StatusActionListener listener, boolean progress) {
setup(progress);
loadMoreButton.setOnClickListener(v -> { loadMoreButton.setOnClickListener(v -> {
loadMoreButton.setEnabled(false); loadMoreButton.setEnabled(false);
listener.onLoadMore(getAdapterPosition()); listener.onLoadMore(getAdapterPosition());
}); });
} }
public void setup(final ChatActionListener listener, boolean progress) {
setup(progress);
loadMoreButton.setOnClickListener( v -> {
loadMoreButton.setEnabled(false);
listener.onLoadMore(getAdapterPosition());
});
}
} }

View File

@ -34,24 +34,14 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
private var pollOptions: List<PollOptionViewData> = emptyList() private var pollOptions: List<PollOptionViewData> = emptyList()
private var voteCount: Int = 0 private var voteCount: Int = 0
private var votersCount: Int? = null
private var mode = RESULT private var mode = RESULT
private var emojis: List<Emoji> = emptyList() private var emojis: List<Emoji> = emptyList()
private var resultClickListener: View.OnClickListener? = null
fun setup( fun setup(options: List<PollOptionViewData>, voteCount: Int, emojis: List<Emoji>, mode: Int) {
options: List<PollOptionViewData>,
voteCount: Int,
votersCount: Int?,
emojis: List<Emoji>,
mode: Int,
resultClickListener: View.OnClickListener?) {
this.pollOptions = options this.pollOptions = options
this.voteCount = voteCount this.voteCount = voteCount
this.votersCount = votersCount
this.emojis = emojis this.emojis = emojis
this.mode = mode this.mode = mode
this.resultClickListener = resultClickListener
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -79,7 +69,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
when(mode) { when(mode) {
RESULT -> { RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount) val percent = calculatePercent(option.votesCount, voteCount)
val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context)
.emojify(emojis, holder.resultTextView) .emojify(emojis, holder.resultTextView)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
@ -87,7 +77,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
val level = percent * 100 val level = percent * 100
holder.resultTextView.background.level = level holder.resultTextView.background.level = level
holder.resultTextView.setOnClickListener(resultClickListener)
} }
SINGLE -> { SINGLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton)

View File

@ -17,7 +17,6 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.util.Log; import android.util.Log;
import android.graphics.Paint;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -70,7 +69,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private TextView displayName; private TextView displayName;
private TextView username; private TextView username;
private TextView replyInfo;
private ImageButton replyButton; private ImageButton replyButton;
private SparkButton reblogButton; private SparkButton reblogButton;
private SparkButton favouriteButton; private SparkButton favouriteButton;
@ -123,7 +121,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
timestampInfo = itemView.findViewById(R.id.status_timestamp_info); timestampInfo = itemView.findViewById(R.id.status_timestamp_info);
content = itemView.findViewById(R.id.status_content); content = itemView.findViewById(R.id.status_content);
avatar = itemView.findViewById(R.id.status_avatar); avatar = itemView.findViewById(R.id.status_avatar);
replyInfo = itemView.findViewById(R.id.reply_info);
replyButton = itemView.findViewById(R.id.status_reply); replyButton = itemView.findViewById(R.id.status_reply);
reblogButton = itemView.findViewById(R.id.status_inset); reblogButton = itemView.findViewById(R.id.status_inset);
favouriteButton = itemView.findViewById(R.id.status_favourite); favouriteButton = itemView.findViewById(R.id.status_favourite);
@ -382,24 +379,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
protected void setReplyInfo(StatusViewData.Concrete status, StatusActionListener listener) {
if (status.getInReplyToId() != null) {
Context context = replyInfo.getContext();
String replyToAccount = status.getInReplyToAccountAcct();
replyInfo.setText(context.getString(R.string.status_replied_to_format, replyToAccount));
if (!status.getParentVisible()) {
replyInfo.setPaintFlags(replyInfo.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
replyInfo.setOnClickListener(null);
} else {
replyInfo.setPaintFlags(replyInfo.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
replyInfo.setOnClickListener(v -> listener.onViewReplyTo(getAdapterPosition()));
}
replyInfo.setVisibility(View.VISIBLE);
} else {
replyInfo.setVisibility(View.GONE);
}
}
private void setReblogged(boolean reblogged) { private void setReblogged(boolean reblogged) {
reblogButton.setChecked(reblogged); reblogButton.setChecked(reblogged);
} }
@ -778,7 +757,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setUsername(status.getNickname()); setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
setReplyInfo(status, listener);
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions); setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
setReblogged(status.isReblogged()); setReblogged(status.isReblogged());
setFavourited(status.isFavourited()); setFavourited(status.isFavourited());
@ -827,7 +805,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
itemView.setAccessibilityDelegate(null); itemView.setAccessibilityDelegate(null);
} else { } else {
if (payloads instanceof List) if (payloads instanceof List)
for (Object item : (List<?>) payloads) { for (Object item : (List) payloads) {
if (Key.KEY_CREATED.equals(item)) { if (Key.KEY_CREATED.equals(item)) {
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
} }
@ -946,7 +924,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
List<PollOptionViewData> options = poll.getOptions(); List<PollOptionViewData> options = poll.getOptions();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
if (i < options.size()) { if (i < options.size()) {
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount()); int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotesCount());
args[i] = buildDescription(options.get(i).getTitle(), percent, context); args[i] = buildDescription(options.get(i).getTitle(), percent, context);
} else { } else {
args[i] = ""; args[i] = "";
@ -989,18 +967,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (expired || poll.getVoted()) { if (expired || poll.getVoted()) {
// no voting possible // no voting possible
View.OnClickListener viewThreadListener = v -> { pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT);
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onViewThread(position);
}
};
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener);
pollButton.setVisibility(View.GONE); pollButton.setVisibility(View.GONE);
} else { } else {
// voting possible // voting possible
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
pollButton.setVisibility(View.VISIBLE); pollButton.setVisibility(View.VISIBLE);
@ -1027,15 +999,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private CharSequence getPollInfoText(long timestamp, PollViewData poll, private CharSequence getPollInfoText(long timestamp, PollViewData poll,
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
Context context) { Context context) {
String votes = numberFormat.format(poll.getVotesCount());
String votesText; String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
if(poll.getVotersCount() == null) {
String voters = numberFormat.format(poll.getVotesCount());
votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters);
} else {
String voters = numberFormat.format(poll.getVotersCount());
votesText = context.getResources().getQuantityString(R.plurals.poll_info_people, poll.getVotersCount(), voters);
}
CharSequence pollDurationInfo; CharSequence pollDurationInfo;
if (poll.getExpired()) { if (poll.getExpired()) {
pollDurationInfo = context.getString(R.string.poll_info_closed); pollDurationInfo = context.getString(R.string.poll_info_closed);
@ -1045,7 +1010,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (statusDisplayOptions.useAbsoluteTime()) { if (statusDisplayOptions.useAbsoluteTime()) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt())); pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
} else { } else {
pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); String pollDuration = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration);
} }
} }

View File

@ -36,8 +36,8 @@ interface ItemInteractionListener {
fun onTabRemoved(position: Int) fun onTabRemoved(position: Int)
fun onStartDelete(viewHolder: RecyclerView.ViewHolder) fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
fun onStartDrag(viewHolder: RecyclerView.ViewHolder) fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
fun onActionChipClicked(tab: TabData, tabPosition: Int) fun onActionChipClicked(tab: TabData)
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) fun onChipClicked(tab: TabData, chipPosition: Int)
} }
class TabAdapter(private var data: List<TabData>, class TabAdapter(private var data: List<TabData>,
@ -62,17 +62,16 @@ class TabAdapter(private var data: List<TabData>,
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val context = holder.itemView.context val context = holder.itemView.context
val tab = data[position] if (!small && data[position].id == LIST) {
if (!small && tab.id == LIST) { holder.itemView.textView.text = data[position].arguments.getOrNull(1).orEmpty()
holder.itemView.textView.text = tab.arguments.getOrNull(1).orEmpty()
} else { } else {
holder.itemView.textView.setText(tab.text) holder.itemView.textView.setText(data[position].text)
} }
val iconDrawable = ThemeUtils.getTintedDrawable(context, tab.icon, android.R.attr.textColorSecondary) val iconDrawable = ThemeUtils.getTintedDrawable(context, data[position].icon, android.R.attr.textColorSecondary)
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null) holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
if (small) { if (small) {
holder.itemView.textView.setOnClickListener { holder.itemView.textView.setOnClickListener {
listener.onTabAdded(tab) listener.onTabAdded(data[position])
} }
} }
holder.itemView.imageView?.setOnTouchListener { _, event -> holder.itemView.imageView?.setOnTouchListener { _, event ->
@ -97,7 +96,7 @@ class TabAdapter(private var data: List<TabData>,
if (!small) { if (!small) {
if (tab.id == HASHTAG) { if (data[position].id == HASHTAG) {
holder.itemView.chipGroup.show() holder.itemView.chipGroup.show()
/* /*
@ -105,33 +104,34 @@ class TabAdapter(private var data: List<TabData>,
* The other dynamic chips are inserted in front of the actionChip. * The other dynamic chips are inserted in front of the actionChip.
* This code tries to reuse already added chips to reduce the number of Views created. * This code tries to reuse already added chips to reduce the number of Views created.
*/ */
tab.arguments.forEachIndexed { i, arg -> data[position].arguments.forEachIndexed { i, arg ->
val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply { ?: Chip(context).apply {
text = arg
holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1) holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1)
} }
chip.text = arg chip.text = arg
if(tab.arguments.size <= 1) { if(data[position].arguments.size <= 1) {
chip.chipIcon = null chip.chipIcon = null
chip.setOnClickListener(null) chip.setOnClickListener(null)
} else { } else {
val cancelIcon = ThemeUtils.getTintedDrawable(context, R.drawable.ic_cancel_24dp, android.R.attr.textColorPrimary) val cancelIcon = ThemeUtils.getTintedDrawable(context, R.drawable.ic_cancel_24dp, android.R.attr.textColorPrimary)
chip.chipIcon = cancelIcon chip.chipIcon = cancelIcon
chip.setOnClickListener { chip.setOnClickListener {
listener.onChipClicked(tab, holder.adapterPosition, i) listener.onChipClicked(data[position], i)
} }
} }
} }
while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { while(holder.itemView.chipGroup.size - 1 > data[position].arguments.size) {
holder.itemView.chipGroup.removeViewAt(tab.arguments.size) holder.itemView.chipGroup.removeViewAt(data[position].arguments.size - 1)
} }
holder.itemView.actionChip.setOnClickListener { holder.itemView.actionChip.setOnClickListener {
listener.onActionChipClicked(tab, holder.adapterPosition) listener.onActionChipClicked(data[position])
} }
} else { } else {
@ -140,7 +140,9 @@ class TabAdapter(private var data: List<TabData>,
} }
} }
override fun getItemCount() = data.size override fun getItemCount(): Int {
return data.size
}
fun setRemoveButtonVisible(enabled: Boolean) { fun setRemoveButtonVisible(enabled: Boolean) {
if (removeButtonEnabled != enabled) { if (removeButtonEnabled != enabled) {

View File

@ -2,7 +2,6 @@ package com.keylesspalace.tusky.appstore
import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.ChatMessage
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
@ -23,5 +22,3 @@ data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
data class DomainMuteEvent(val instance: String): Dispatchable data class DomainMuteEvent(val instance: String): Dispatchable
data class ChatMessageDeliveredEvent(val chatMsg: ChatMessage) : Dispatchable
data class ChatMessageReceivedEvent(val chatMsg: ChatMessage) : Dispatchable

View File

@ -1,29 +0,0 @@
package com.keylesspalace.tusky.components.chat
import com.keylesspalace.tusky.components.common.CommonComposeViewModel
import com.keylesspalace.tusky.components.common.MediaUploader
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.util.*
import javax.inject.Inject
open class ChatViewModel
@Inject constructor(
private val api: MastodonApi,
private val accountManager: AccountManager,
private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient,
private val saveTootHelper: SaveTootHelper,
private val db: AppDatabase
) : CommonComposeViewModel(api, accountManager, mediaUploader, db) {
fun getSingleMedia() : ComposeActivity.QueuedMedia? {
return if(media.value?.isNotEmpty() == true)
media.value?.get(0)
else null
}
}

View File

@ -1,378 +0,0 @@
/* Copyright 2019 Tusky Contributors
*
* 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.components.common
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.InstanceEntity
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Singles
import retrofit2.Response
import java.util.*
import javax.inject.Inject
/**
* Throw when trying to add an image when video is already present or the other way around
*/
class VideoOrImageException : Exception()
open class CommonComposeViewModel(
private val api: MastodonApi,
private val accountManager: AccountManager,
private val mediaUploader: MediaUploader,
private val db: AppDatabase
) : RxAwareViewModel() {
protected val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
protected val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData(null)
protected val stickers: MutableLiveData<Array<StickerPack>> = MutableLiveData(emptyArray())
val haveStickers: MutableLiveData<Boolean> = MutableLiveData(false)
var tryFetchStickers = false
var anonymizeNames = true
var hasNoAttachmentLimits = false
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
ComposeInstanceParams(
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
chatLimit = instance?.chatLimit ?: DEFAULT_CHARACTER_LIMIT,
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
)
}
val instanceMetadata: LiveData<ComposeInstanceMetadata> = nodeinfo.map { nodeinfo ->
val software = nodeinfo?.software?.name ?: "mastodon"
if(software.equals("pleroma")) {
hasNoAttachmentLimits = true
ComposeInstanceMetadata(
software = "pleroma",
supportsMarkdown = nodeinfo?.metadata?.postFormats?.contains("text/markdown") ?: false,
supportsBBcode = nodeinfo?.metadata?.postFormats?.contains("text/bbcode") ?: false,
supportsHTML = nodeinfo?.metadata?.postFormats?.contains("text/html") ?: false,
videoLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_IMAGE_SIZE_LIMIT
)
} else if(software.equals("pixelfed")) {
ComposeInstanceMetadata(
software = "pixelfed",
supportsMarkdown = false,
supportsBBcode = false,
supportsHTML = false,
videoLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_IMAGE_SIZE_LIMIT
)
} else {
ComposeInstanceMetadata(
software = "mastodon",
supportsMarkdown = nodeinfo?.software?.version?.contains("+glitch") ?: false,
supportsBBcode = false,
supportsHTML = nodeinfo?.software?.version?.contains("+glitch") ?: false,
videoLimit = STATUS_VIDEO_SIZE_LIMIT,
imageLimit = STATUS_IMAGE_SIZE_LIMIT
)
}
}
val instanceStickers: LiveData<Array<StickerPack>> = stickers // .map { stickers -> HashMap<String,String>(stickers) }
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
val media = mutableLiveData<List<QueuedMedia>>(listOf())
val uploadError = MutableLiveData<Throwable>()
protected val mediaToDisposable = mutableMapOf<Long, Disposable>()
init {
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
InstanceEntity(
instance = accountManager.activeAccount?.domain!!,
emojiList = emojis,
maximumTootCharacters = instance.maxTootChars,
maxPollOptions = instance.pollLimits?.maxOptions,
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
version = instance.version,
chatLimit = instance.chatLimit
)
}
.doOnSuccess {
db.instanceDao().insertOrReplace(it)
}
.onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
)
.subscribe ({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity)
}, { throwable ->
// this can happen on network error when no cached data is available
Log.w(TAG, "error loading instance data", throwable)
})
.autoDispose()
api.getNodeinfoLinks().subscribe({
links -> if(links.links.isNotEmpty()) {
api.getNodeinfo(links.links[0].href).subscribe({
ni -> nodeinfo.postValue(ni)
}, {
err -> Log.d(TAG, "Failed to get nodeinfo", err)
}).autoDispose()
}
}, { err ->
Log.d(TAG, "Failed to get nodeinfo links", err)
}).autoDispose()
}
fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> {
// We are not calling .toLiveData() here because we don't want to stop the process when
// the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
mediaUploader.prepareMedia(uri, videoLimit, imageLimit, filename)
.map { (type, uri, size) ->
val mediaItems = media.value!!
if (!hasNoAttachmentLimits
&& type != QueuedMedia.Type.IMAGE
&& mediaItems.isNotEmpty()
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException()
} else {
addMediaToQueue(type, uri, size, filename ?: "unknown", anonymizeNames)
}
}
.subscribe({ queuedMedia ->
liveData.postValue(Either.Right(queuedMedia))
}, { error ->
liveData.postValue(Either.Left(error))
})
.autoDispose()
return liveData
}
private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String, anonymizeNames: Boolean): QueuedMedia {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename,
hasNoAttachmentLimits, anonymizeNames)
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem, videoLimit, imageLimit )
.subscribe ({ event ->
val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe
val newMediaItem = when (event) {
is UploadEvent.ProgressEvent ->
item.copy(uploadPercent = event.percentage)
is UploadEvent.FinishedEvent ->
item.copy(id = event.attachment.id, uploadPercent = -1)
}
synchronized(media) {
val mediaValue = media.value!!
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
media.postValue(if (index == -1) {
mediaValue + newMediaItem
} else {
mediaValue.toMutableList().also { it[index] = newMediaItem }
})
}
}, { error ->
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
uploadError.postValue(error)
})
return mediaItem
}
protected fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown",
hasNoAttachmentLimits, anonymizeNames, -1, id, description)
media.value = media.value!! + mediaItem
}
fun removeMediaFromQueue(item: QueuedMedia) {
mediaToDisposable[item.localId]?.dispose()
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
}
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
val newList = media.value!!.toMutableList()
val index = newList.indexOfFirst { it.localId == localId }
if (index != -1) {
newList[index] = newList[index].copy(description = description)
}
media.value = newList
val completedCaptioningLiveData = MutableLiveData<Boolean>()
media.observeForever(object : Observer<List<QueuedMedia>> {
override fun onChanged(mediaItems: List<QueuedMedia>) {
val updatedItem = mediaItems.find { it.localId == localId }
if (updatedItem == null) {
media.removeObserver(this)
} else if (updatedItem.id != null) {
api.updateMedia(updatedItem.id, description)
.subscribe({
completedCaptioningLiveData.postValue(true)
}, {
completedCaptioningLiveData.postValue(false)
})
.autoDispose()
media.removeObserver(this)
}
}
})
return completedCaptioningLiveData
}
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
when (token[0]) {
'@' -> {
return try {
api.searchAccounts(query = token.substring(1), limit = 10)
.blockingGet()
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
} catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList()
}
}
'#' -> {
return try {
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
.blockingGet()
.hashtags
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
} catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList()
}
}
':' -> {
val emojiList = emoji.value ?: return emptyList()
val incomplete = token.substring(1).toLowerCase(Locale.ROOT)
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
for (emoji in emojiList) {
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
if (shortcode.startsWith(incomplete)) {
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
} else if (shortcode.indexOf(incomplete, 1) != -1) {
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
}
}
if (results.isNotEmpty() && resultsInside.isNotEmpty()) {
results.add(ComposeAutoCompleteAdapter.ResultSeparator())
}
results.addAll(resultsInside)
return results
}
else -> {
Log.w(TAG, "Unexpected autocompletion token: $token")
return emptyList()
}
}
}
override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose()
}
super.onCleared()
}
private fun getStickers() {
if(!tryFetchStickers)
return
api.getStickers().subscribe({ stickers ->
if (stickers.isNotEmpty()) {
haveStickers.postValue(true)
val singles = mutableListOf<Single<Response<StickerPack>>>()
for(entry in stickers) {
val url = entry.value.removePrefix("/").removeSuffix("/") + "/pack.json";
singles += api.getStickerPack(url)
}
Single.zip(singles) {
it.map {
it as Response<StickerPack>
it.body()!!.internal_url = it.raw().request.url.toString().removeSuffix("pack.json")
it.body()!!
}
}.onErrorReturn {
Log.d(TAG, "Failed to get sticker pack.json", it)
emptyList()
}.subscribe() { pack ->
if(pack.isNotEmpty()) {
val array = pack.toTypedArray()
array.sort()
this.stickers.postValue(array)
}
}.autoDispose()
}
}, {
err -> Log.d(TAG, "Failed to get sticker.json", err)
}).autoDispose()
}
fun setup() {
getStickers() // early as possible
}
private companion object {
const val TAG = "CCVM"
}
}
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
const val DEFAULT_CHARACTER_LIMIT = 500
const val DEFAULT_MAX_OPTION_COUNT = 4
const val DEFAULT_MAX_OPTION_LENGTH = 25
const val STATUS_VIDEO_SIZE_LIMIT : Long = 41943040 // 40MiB
const val STATUS_IMAGE_SIZE_LIMIT : Long = 8388608 // 8MiB
data class ComposeInstanceParams(
val maxChars: Int,
val chatLimit: Int,
val pollMaxOptions: Int,
val pollMaxLength: Int,
val supportsScheduled: Boolean
)
data class ComposeInstanceMetadata(
val software: String,
val supportsMarkdown: Boolean,
val supportsBBcode: Boolean,
val supportsHTML: Boolean,
val videoLimit: Long,
val imageLimit: Long
)

View File

@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.ContentResolver
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -52,6 +53,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -59,6 +61,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -69,7 +72,6 @@ import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.components.common.*
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
@ -80,7 +82,6 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.EmojiKeyboard import com.keylesspalace.tusky.view.EmojiKeyboard
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
@ -149,7 +150,6 @@ class ComposeActivity : BaseActivity(),
val activeAccount = accountManager.activeAccount ?: return val activeAccount = accountManager.activeAccount ?: return
viewModel.tryFetchStickers = preferences.getBoolean("stickers", false) viewModel.tryFetchStickers = preferences.getBoolean("stickers", false)
viewModel.anonymizeNames = preferences.getBoolean(PrefKeys.ANONYMIZE_FILENAMES, false)
setupAvatar(preferences, activeAccount) setupAvatar(preferences, activeAccount)
val mediaAdapter = MediaPreviewAdapter( val mediaAdapter = MediaPreviewAdapter(
this, this,
@ -210,6 +210,35 @@ class ComposeActivity : BaseActivity(),
} }
} }
private fun uriToFilename(uri: Uri): String {
var result: String = "unknown"
if(uri.scheme.equals("content")) {
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.let {
try {
if(cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
finally {
cursor.close()
}
}
}
if(result.equals("unknown")) {
val path = uri.getPath()
path?.let {
result = path
val cut = result.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
}
return result
}
private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) { private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) {
if (intent != null && savedInstanceState == null) { if (intent != null && savedInstanceState == null) {
/* Get incoming images being sent through a share action from another app. Only do this /* Get incoming images being sent through a share action from another app. Only do this
@ -1100,7 +1129,7 @@ class ComposeActivity : BaseActivity(),
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) { private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) {
withLifecycleContext { withLifecycleContext {
viewModel.pickMedia(uri, filename ?: uri.toFileName(contentResolver)).observe { exceptionOrItem -> viewModel.pickMedia(uri, filename ?: uriToFilename(uri)).observe { exceptionOrItem ->
contentInfoCompat?.releasePermission() contentInfoCompat?.releasePermission()
@ -1271,7 +1300,6 @@ class ComposeActivity : BaseActivity(),
val mediaSize: Long, val mediaSize: Long,
val originalFileName: String, val originalFileName: String,
val noChanges: Boolean = false, val noChanges: Boolean = false,
val anonymizeFileName: Boolean = false,
val uploadPercent: Int = 0, val uploadPercent: Int = 0,
val id: String? = null, val id: String? = null,
val description: String? = null val description: String? = null
@ -1317,8 +1345,7 @@ class ComposeActivity : BaseActivity(),
var scheduledAt: String? = null, var scheduledAt: String? = null,
var sensitive: Boolean? = null, var sensitive: Boolean? = null,
var poll: NewPoll? = null, var poll: NewPoll? = null,
var formattingSyntax: String? = null, var formattingSyntax: String? = null
var modifiedInitialState: Boolean? = null
) : Parcelable ) : Parcelable
companion object { companion object {

View File

@ -22,10 +22,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.components.common.CommonComposeViewModel
import com.keylesspalace.tusky.components.common.MediaUploader
import com.keylesspalace.tusky.components.common.UploadEvent
import com.keylesspalace.tusky.components.common.mutableLiveData
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
@ -40,10 +36,17 @@ import com.keylesspalace.tusky.util.*
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Singles import io.reactivex.rxkotlin.Singles
import io.reactivex.schedulers.Schedulers
import retrofit2.Response import retrofit2.Response
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
/**
* Throw when trying to add an image when video is already present or the other way around
*/
class VideoOrImageException : Exception()
class ComposeViewModel class ComposeViewModel
@Inject constructor( @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
@ -52,7 +55,7 @@ class ComposeViewModel
private val serviceClient: ServiceClient, private val serviceClient: ServiceClient,
private val saveTootHelper: SaveTootHelper, private val saveTootHelper: SaveTootHelper,
private val db: AppDatabase private val db: AppDatabase
) : CommonComposeViewModel(api, accountManager, mediaUploader, db) { ) : RxAwareViewModel() {
private var replyingStatusAuthor: String? = null private var replyingStatusAuthor: String? = null
private var replyingStatusContent: String? = null private var replyingStatusContent: String? = null
@ -62,9 +65,58 @@ class ComposeViewModel
private var inReplyToId: String? = null private var inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
private var contentWarningStateChanged: Boolean = false private var contentWarningStateChanged: Boolean = false
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
private val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData(null)
private val stickers: MutableLiveData<Array<StickerPack>> = MutableLiveData(emptyArray())
public val haveStickers: MutableLiveData<Boolean> = MutableLiveData(false)
public var tryFetchStickers = false
public var formattingSyntax: String = "" public var formattingSyntax: String = ""
private var modifiedInitialState: Boolean = false public var hasNoAttachmentLimits = false
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
ComposeInstanceParams(
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
)
}
val instanceMetadata: LiveData<ComposeInstanceMetadata> = nodeinfo.map { nodeinfo ->
val software = nodeinfo?.software?.name ?: "mastodon"
if(software.equals("pleroma")) {
hasNoAttachmentLimits = true
ComposeInstanceMetadata(
software = "pleroma",
supportsMarkdown = nodeinfo?.metadata?.postFormats?.contains("text/markdown") ?: false,
supportsBBcode = nodeinfo?.metadata?.postFormats?.contains("text/bbcode") ?: false,
supportsHTML = nodeinfo?.metadata?.postFormats?.contains("text/html") ?: false,
videoLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_IMAGE_SIZE_LIMIT
)
} else if(software.equals("pixelfed")) {
ComposeInstanceMetadata(
software = "pixelfed",
supportsMarkdown = false,
supportsBBcode = false,
supportsHTML = false,
videoLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize?.let { it * 1024 } ?: STATUS_IMAGE_SIZE_LIMIT
)
} else {
ComposeInstanceMetadata(
software = "mastodon",
supportsMarkdown = nodeinfo?.software?.version?.contains("+glitch") ?: false,
supportsBBcode = false,
supportsHTML = nodeinfo?.software?.version?.contains("+glitch") ?: false,
videoLimit = STATUS_VIDEO_SIZE_LIMIT,
imageLimit = STATUS_IMAGE_SIZE_LIMIT
)
}
}
val instanceStickers: LiveData<Array<StickerPack>> = stickers // .map { stickers -> HashMap<String,String>(stickers) }
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
val markMediaAsSensitive = val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
@ -77,6 +129,125 @@ class ComposeViewModel
val setupComplete = mutableLiveData(false) val setupComplete = mutableLiveData(false)
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null) val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null) val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
val media = mutableLiveData<List<QueuedMedia>>(listOf())
val uploadError = MutableLiveData<Throwable>()
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
init {
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
InstanceEntity(
instance = accountManager.activeAccount?.domain!!,
emojiList = emojis,
maximumTootCharacters = instance.maxTootChars,
maxPollOptions = instance.pollLimits?.maxOptions,
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
version = instance.version
)
}
.doOnSuccess {
db.instanceDao().insertOrReplace(it)
}
.onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
)
.subscribe ({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity)
}, { throwable ->
// this can happen on network error when no cached data is available
Log.w(TAG, "error loading instance data", throwable)
})
.autoDispose()
api.getNodeinfoLinks().subscribe({
links -> if(links.links.isNotEmpty()) {
api.getNodeinfo(links.links[0].href).subscribe({
ni -> nodeinfo.postValue(ni)
}, {
err -> Log.d(TAG, "Failed to get nodeinfo", err)
}).autoDispose()
}
}, { err ->
Log.d(TAG, "Failed to get nodeinfo links", err)
}).autoDispose()
}
fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> {
// We are not calling .toLiveData() here because we don't want to stop the process when
// the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
mediaUploader.prepareMedia(uri, videoLimit, imageLimit, filename)
.map { (type, uri, size) ->
val mediaItems = media.value!!
if (!hasNoAttachmentLimits
&& type != QueuedMedia.Type.IMAGE
&& mediaItems.isNotEmpty()
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException()
} else {
addMediaToQueue(type, uri, size, filename ?: "unknown")
}
}
.subscribe({ queuedMedia ->
liveData.postValue(Either.Right(queuedMedia))
}, { error ->
liveData.postValue(Either.Left(error))
})
.autoDispose()
return liveData
}
private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String): QueuedMedia {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename,
hasNoAttachmentLimits)
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem, videoLimit, imageLimit )
.subscribe ({ event ->
val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe
val newMediaItem = when (event) {
is UploadEvent.ProgressEvent ->
item.copy(uploadPercent = event.percentage)
is UploadEvent.FinishedEvent ->
item.copy(id = event.attachment.id, uploadPercent = -1)
}
synchronized(media) {
val mediaValue = media.value!!
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
media.postValue(if (index == -1) {
mediaValue + newMediaItem
} else {
mediaValue.toMutableList().also { it[index] = newMediaItem }
})
}
}, { error ->
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
uploadError.postValue(error)
})
return mediaItem
}
private fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown",
hasNoAttachmentLimits, -1, id, description)
media.value = media.value!! + mediaItem
}
fun removeMediaFromQueue(item: QueuedMedia) {
mediaToDisposable[item.localId]?.dispose()
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
}
fun didChange(content: String?, contentWarning: String?): Boolean { fun didChange(content: String?, contentWarning: String?): Boolean {
@ -89,7 +260,7 @@ class ComposeViewModel
val mediaChanged = !media.value.isNullOrEmpty() val mediaChanged = !media.value.isNullOrEmpty()
val pollChanged = poll.value != null val pollChanged = poll.value != null
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged return textChanged || contentWarningChanged || mediaChanged || pollChanged
} }
fun contentWarningChanged(value: Boolean) { fun contentWarningChanged(value: Boolean) {
@ -171,6 +342,85 @@ class ComposeViewModel
} }
} }
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
val newList = media.value!!.toMutableList()
val index = newList.indexOfFirst { it.localId == localId }
if (index != -1) {
newList[index] = newList[index].copy(description = description)
}
media.value = newList
val completedCaptioningLiveData = MutableLiveData<Boolean>()
media.observeForever(object : Observer<List<QueuedMedia>> {
override fun onChanged(mediaItems: List<QueuedMedia>) {
val updatedItem = mediaItems.find { it.localId == localId }
if (updatedItem == null) {
media.removeObserver(this)
} else if (updatedItem.id != null) {
api.updateMedia(updatedItem.id, description)
.subscribe({
completedCaptioningLiveData.postValue(true)
}, {
completedCaptioningLiveData.postValue(false)
})
.autoDispose()
media.removeObserver(this)
}
}
})
return completedCaptioningLiveData
}
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
when (token[0]) {
'@' -> {
return try {
api.searchAccounts(query = token.substring(1), limit = 10)
.blockingGet()
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
} catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList()
}
}
'#' -> {
return try {
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
.blockingGet()
.hashtags
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
} catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList()
}
}
':' -> {
val emojiList = emoji.value ?: return emptyList()
val incomplete = token.substring(1).toLowerCase(Locale.ROOT)
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
for (emoji in emojiList) {
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
if (shortcode.startsWith(incomplete)) {
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
} else if (shortcode.indexOf(incomplete, 1) != -1) {
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
}
}
if (results.isNotEmpty() && resultsInside.isNotEmpty()) {
results.add(ComposeAutoCompleteAdapter.ResultSeparator())
}
results.addAll(resultsInside)
return results
}
else -> {
Log.w(TAG, "Unexpected autocompletion token: $token")
return emptyList()
}
}
}
override fun onCleared() { override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) { for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose() uploadDisposable.dispose()
@ -178,8 +428,45 @@ class ComposeViewModel
super.onCleared() super.onCleared()
} }
private fun getStickers() {
if(!tryFetchStickers)
return
api.getStickers().subscribe({ stickers ->
if (stickers.isNotEmpty()) {
haveStickers.postValue(true)
val singles = mutableListOf<Single<Response<StickerPack>>>()
for(entry in stickers) {
val url = entry.value.removePrefix("/").removeSuffix("/") + "/pack.json";
singles += api.getStickerPack(url)
}
Single.zip(singles) {
it.map {
it as Response<StickerPack>
it.body()!!.internal_url = it.raw().request.url.toString().removeSuffix("pack.json")
it.body()!!
}
}.onErrorReturn {
Log.d(TAG, "Failed to get sticker pack.json", it)
emptyList()
}.subscribe() { pack ->
if(pack.isNotEmpty()) {
val array = pack.toTypedArray()
array.sort()
this.stickers.postValue(array)
}
}.autoDispose()
}
}, {
err -> Log.d(TAG, "Failed to get sticker.json", err)
}).autoDispose()
}
fun setup(composeOptions: ComposeActivity.ComposeOptions?) { fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
super.setup() getStickers() // early as possible
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
@ -187,7 +474,7 @@ class ComposeViewModel
preferredVisibility.num.coerceAtLeast(replyVisibility.num)) preferredVisibility.num.coerceAtLeast(replyVisibility.num))
inReplyToId = composeOptions?.inReplyToId inReplyToId = composeOptions?.inReplyToId
modifiedInitialState = composeOptions?.modifiedInitialState == true
val contentWarning = composeOptions?.contentWarning val contentWarning = composeOptions?.contentWarning
if (contentWarning != null) { if (contentWarning != null) {
@ -267,4 +554,30 @@ class ComposeViewModel
private companion object { private companion object {
const val TAG = "ComposeViewModel" const val TAG = "ComposeViewModel"
} }
} }
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
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 STATUS_VIDEO_SIZE_LIMIT : Long = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT : Long = 8388608 // 8MiB
data class ComposeInstanceParams(
val maxChars: Int,
val pollMaxOptions: Int,
val pollMaxLength: Int,
val supportsScheduled: Boolean
)
data class ComposeInstanceMetadata(
val software: String,
val supportsMarkdown: Boolean,
val supportsBBcode: Boolean,
val supportsHTML: Boolean,
val videoLimit: Long,
val imageLimit: Long
)

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.common; package com.keylesspalace.tusky.components.compose;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.graphics.Bitmap; import android.graphics.Bitmap;

View File

@ -133,6 +133,7 @@ class MediaPreviewAdapter(
view.marqueeRepeatLimit = -1 view.marqueeRepeatLimit = -1
view.setSingleLine() view.setSingleLine()
view.setSelected(true) view.setSelected(true)
view.maxLines = 1
view.textSize = 16.0f view.textSize = 16.0f
view.setOnClickListener { view.setOnClickListener {
onMediaClick(adapterPosition, view) onMediaClick(adapterPosition, view)

View File

@ -13,13 +13,11 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.common package com.keylesspalace.tusky.components.compose
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -46,10 +44,10 @@ sealed class UploadEvent {
data class FinishedEvent(val attachment: Attachment) : UploadEvent() data class FinishedEvent(val attachment: Attachment) : UploadEvent()
} }
fun createNewImageFile(context: Context, name: String = "Photo"): File { fun createNewImageFile(context: Context): File {
// Create an image file name // Create an image file name
val randomId = randomAlphanumericString(4) val randomId = randomAlphanumericString(12)
val imageFileName = "${name}_${randomId}" val imageFileName = "Husky_${randomId}_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile( return File.createTempFile(
imageFileName, /* prefix */ imageFileName, /* prefix */
@ -134,22 +132,22 @@ class MediaUploaderImpl(
if (mediaSize > videoLimit) { if (mediaSize > videoLimit) {
throw VideoSizeException() throw VideoSizeException()
} }
PreparedMedia(QueuedMedia.VIDEO, uri, mediaSize) PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
} }
"image" -> { "image" -> {
PreparedMedia(QueuedMedia.IMAGE, uri, mediaSize) PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
} }
"audio" -> { "audio" -> {
if (mediaSize > videoLimit) { // TODO: CHANGE!!11 if (mediaSize > videoLimit) { // TODO: CHANGE!!11
throw AudioSizeException() throw AudioSizeException()
} }
PreparedMedia(QueuedMedia.AUDIO, uri, mediaSize) PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
} }
else -> { else -> {
if (mediaSize > videoLimit) { if (mediaSize > videoLimit) {
throw MediaSizeException() throw MediaSizeException()
} }
PreparedMedia(QueuedMedia.UNKNOWN, uri, mediaSize) PreparedMedia(QueuedMedia.Type.UNKNOWN, uri, mediaSize)
// throw MediaTypeException() // throw MediaTypeException()
} }
} }
@ -164,8 +162,7 @@ class MediaUploaderImpl(
private fun upload(media: QueuedMedia): Observable<UploadEvent> { private fun upload(media: QueuedMedia): Observable<UploadEvent> {
return Observable.create { emitter -> return Observable.create { emitter ->
var (mimeType, fileExtension) = getMimeTypeAndSuffixFromFilenameOrUri(media.uri, media.originalFileName) var (mimeType, fileExtension) = getMimeTypeAndSuffixFromFilenameOrUri(media.uri, media.originalFileName)
val filename = if(!media.anonymizeFileName) media.originalFileName else val filename = String.format("%s_%s_%s%s",
String.format("%s_%s_%s%s",
context.getString(R.string.app_name), context.getString(R.string.app_name),
Date().time.toString(), Date().time.toString(),
randomAlphanumericString(10), randomAlphanumericString(10),
@ -200,7 +197,7 @@ class MediaUploaderImpl(
} }
private fun downsize(media: QueuedMedia, imageLimit: Long): QueuedMedia { private fun downsize(media: QueuedMedia, imageLimit: Long): QueuedMedia {
val file = createNewImageFile(context, media.originalFileName) val file = createNewImageFile(context)
DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file) DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file)
return media.copy(uri = file.toUri(), mediaSize = file.length()) return media.copy(uri = file.toUri(), mediaSize = file.length())
} }
@ -229,27 +226,3 @@ class MediaUploaderImpl(
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
} }
} }
fun Uri.toFileName(contentResolver: ContentResolver? = null): String {
var result: String = "unknown"
if(scheme.equals("content") && contentResolver != null) {
val cursor = contentResolver.query(this, null, null, null, null)
cursor?.use{
if(it.moveToFirst()) {
result = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
if(result.equals("unknown")) {
path?.let {
result = it
val cut = result.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
}
return result
}

View File

@ -35,14 +35,13 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.github.piasy.biv.loader.glide.GlideCustomImageLoader import com.github.piasy.biv.loader.glide.GlideCustomImageLoader
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import com.github.piasy.biv.loader.ImageLoader
import com.github.piasy.biv.view.GlideImageViewFactory import com.github.piasy.biv.view.GlideImageViewFactory
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.withLifecycleContext import com.keylesspalace.tusky.util.withLifecycleContext
import java.io.File
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 // https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
fun <T> T.makeCaptionDialog(existingDescription: String?, fun <T> T.makeCaptionDialog(existingDescription: String?,
previewUri: Uri, previewUri: Uri,
@ -54,19 +53,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
dialogLayout.orientation = LinearLayout.VERTICAL dialogLayout.orientation = LinearLayout.VERTICAL
val imageView = BigImageView(this) val imageView = BigImageView(this)
// imageView.ssiv.maxScale = 6f
imageView.setImageViewFactory(GlideImageViewFactory()) imageView.setImageViewFactory(GlideImageViewFactory())
imageView.setImageLoaderCallback(object : ImageLoader.Callback {
override fun onSuccess(image: File?) {
imageView.ssiv?.let { it.maxScale = 6f }
}
override fun onFail(error: Exception?) {}
override fun onStart() {}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
})
imageView.showImage(previewUri)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics) windowManager.defaultDisplay.getMetrics(displayMetrics)
@ -83,9 +71,7 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
dialogLayout.addView(input) dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2) input.setLines(2)
input.inputType = (InputType.TYPE_CLASS_TEXT input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
input.setText(existingDescription) input.setText(existingDescription)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
@ -111,8 +97,12 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
dialog.show() dialog.show()
// Load the image and manually set it into the ImageView because it doesn't have a fixed
// size. Maybe we should limit the size of CustomTarget
imageView.showImage(previewUri)
} }
private fun Activity.showFailedCaptionMessage() { private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
} }

View File

@ -91,8 +91,8 @@ public final class ProgressImageView extends AppCompatImageView {
super.onDraw(canvas); super.onDraw(canvas);
float angle = (progress / 100f) * 360 - 90; float angle = (progress / 100f) * 360 - 90;
float halfWidth = getWidth() / 2.0f; float halfWidth = getWidth() / 2;
float halfHeight = getHeight() / 2.0f; float halfHeight = getHeight() / 2;
progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f);
biggerRect.set(progressRect); biggerRect.set(progressRect);
int margin = 8; int margin = 8;

View File

@ -90,8 +90,8 @@ public final class ProgressTextView extends TextView {
canvas.translate(getScrollX(), 0); canvas.translate(getScrollX(), 0);
float angle = (progress / 100f) * 360 - 90; float angle = (progress / 100f) * 360 - 90;
float halfWidth = getWidth() / 2.0f; float halfWidth = getWidth() / 2;
float halfHeight = getHeight() / 2.0f; float halfHeight = getHeight() / 2;
progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f);
biggerRect.set(progressRect); biggerRect.set(progressRect);
int margin = 8; int margin = 8;

View File

@ -102,6 +102,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
viewModel.refresh() viewModel.refresh()
} }
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
} }
private fun onTopLoaded() { private fun onTopLoaded() {
@ -138,10 +139,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
} }
} }
override fun onViewReplyTo(position: Int) {
// there are no Reply to labels in conversations
}
override fun onOpenReblog(position: Int) { override fun onOpenReblog(position: Int) {
// there are no reblogs in search results // there are no reblogs in search results
} }

View File

@ -1,84 +0,0 @@
package com.keylesspalace.tusky.components.notifications
import android.util.Log
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isLessThan
import javax.inject.Inject
class NotificationFetcher @Inject constructor(
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val notifier: Notifier
) {
fun fetchAndShow() {
for (account in accountManager.getAllAccountsOrderedByActive()) {
if (account.notificationsEnabled) {
try {
val notifications = fetchNotifications(account)
notifications.forEachIndexed { index, notification ->
notifier.show(notification, account, index == 0)
}
accountManager.saveAccount(account)
} catch (e: Exception) {
Log.w(TAG, "Error while fetching notifications", e)
}
}
}
}
private fun fetchNotifications(account: AccountEntity): MutableList<Notification> {
val authHeader = String.format("Bearer %s", account.accessToken)
// We fetch marker to not load/show notifications which user has already seen
val marker = fetchMarker(authHeader, account)
if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) {
account.lastNotificationId = marker.lastReadId
}
Log.d(TAG, "getting Notifications for " + account.fullName)
val notifications = mastodonApi.notificationsWithAuth(
authHeader,
account.domain,
account.lastNotificationId,
true,
Notification.Type.asStringList
).blockingGet()
val newId = account.lastNotificationId
var newestId = ""
val result = mutableListOf<Notification>()
for (notification in notifications.reversed()) {
val currentId = notification.id
if (newestId.isLessThan(currentId)) {
newestId = currentId
account.lastNotificationId = currentId
}
if (newId.isLessThan(currentId)) {
result.add(notification)
}
}
return result
}
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
return try {
val allMarkers = mastodonApi.markersWithAuth(
authHeader,
account.domain,
listOf("notifications")
).blockingGet()
val notificationMarker = allMarkers["notifications"]
Log.d(TAG, "Fetched marker: $notificationMarker")
notificationMarker
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch marker", e)
null
}
}
companion object {
const val TAG = "NotificationFetcher"
}
}

View File

@ -52,7 +52,6 @@ import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.ChatMessage;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.PollOption;
@ -91,8 +90,6 @@ public class NotificationHelper {
public static final String COMPOSE_ACTION = "COMPOSE_ACTION"; public static final String COMPOSE_ACTION = "COMPOSE_ACTION";
public static final String CHAT_REPLY_ACTION = "CHAT_REPLY_ACTION";
public static final String KEY_REPLY = "KEY_REPLY"; public static final String KEY_REPLY = "KEY_REPLY";
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID"; public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
@ -115,8 +112,6 @@ public class NotificationHelper {
public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL"; public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL";
public static final String KEY_CHAT_ID = "KEY_CHAT_ID";
/** /**
* notification channels used on Android O+ * notification channels used on Android O+
**/ **/
@ -127,7 +122,6 @@ public class NotificationHelper {
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_POLL = "CHANNEL_POLL";
public static final String CHANNEL_EMOJI_REACTION = "CHANNEL_EMOJI_REACTION"; public static final String CHANNEL_EMOJI_REACTION = "CHANNEL_EMOJI_REACTION";
public static final String CHANNEL_CHAT_MESSAGES = "CHANNEL_CHAT_MESSAGES";
/** /**
@ -138,7 +132,7 @@ public class NotificationHelper {
/** /**
* by setting this as false, it's possible to test legacy notification channels on newer devices * by setting this as false, it's possible to test legacy notification channels on newer devices
*/ */
// public static final boolean NOTIFICATION_USE_CHANNELS = false; //public static final boolean NOTIFICATION_USE_CHANNELS = false;
public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
/** /**
@ -157,13 +151,13 @@ public class NotificationHelper {
} }
// Pleroma extension: don't notify about seen notifications // Pleroma extension: don't notify about seen notifications
if (body.getPleroma() != null && body.getPleroma().getSeen()) { if (body.getPleroma() != null && body.getPleroma().getSeen() == true) {
return; return;
} }
if (body.getStatus() != null && if (body.getStatus() != null &&
(body.getStatus().isUserMuted() || (body.getStatus().isUserMuted() == true ||
body.getStatus().isThreadMuted())) { body.getStatus().isThreadMuted() == true)) {
return; return;
} }
@ -224,45 +218,30 @@ public class NotificationHelper {
builder.setLargeIcon(accountAvatar); builder.setLargeIcon(accountAvatar);
// Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (body.getType() == Notification.Type.MENTION
if(body.getType() == Notification.Type.MENTION) { && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(context.getString(R.string.label_quick_reply)) .setLabel(context.getString(R.string.label_quick_reply))
.build(); .build();
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account);
NotificationCompat.Action quickReplyAction = NotificationCompat.Action quickReplyAction =
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
context.getString(R.string.action_quick_reply), quickReplyPendingIntent) context.getString(R.string.action_quick_reply), quickReplyPendingIntent)
.addRemoteInput(replyRemoteInput) .addRemoteInput(replyRemoteInput)
.build(); .build();
builder.addAction(quickReplyAction); builder.addAction(quickReplyAction);
PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account);
NotificationCompat.Action composeAction = NotificationCompat.Action composeAction =
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
context.getString(R.string.action_compose_shortcut), composePendingIntent) context.getString(R.string.action_compose_shortcut), composePendingIntent)
.build(); .build();
builder.addAction(composeAction); builder.addAction(composeAction);
} else if(body.getType() == Notification.Type.CHAT_MESSAGE) {
RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(context.getString(R.string.label_quick_reply))
.build();
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(CHAT_REPLY_ACTION, context, body, account);
NotificationCompat.Action quickReplyAction =
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
context.getString(R.string.action_quick_reply), quickReplyPendingIntent)
.addRemoteInput(replyRemoteInput)
.build();
builder.addAction(quickReplyAction);
}
} }
builder.setSubText(account.getFullName()); builder.setSubText(account.getFullName());
@ -347,40 +326,35 @@ public class NotificationHelper {
} }
private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) { private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) {
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) Status status = body.getStatus();
.setAction(action)
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
.putExtra(KEY_NOTIFICATION_ID, notificationId);
if(action == CHAT_REPLY_ACTION) { String citedLocalAuthor = status.getAccount().getLocalUsername();
replyIntent.putExtra(KEY_CHAT_ID, body.getChatMessage().getChatId()); String citedText = status.getContent().toString();
} else { String inReplyToId = status.getId();
Status status = body.getStatus(); Status actionableStatus = status.getActionableStatus();
Status.Visibility replyVisibility = actionableStatus.getVisibility();
String citedLocalAuthor = status.getAccount().getLocalUsername(); String contentWarning = actionableStatus.getSpoilerText();
String citedText = status.getContent().toString(); Status.Mention[] mentions = actionableStatus.getMentions();
String inReplyToId = status.getId(); List<String> mentionedUsernames = new ArrayList<>();
Status actionableStatus = status.getActionableStatus(); mentionedUsernames.add(actionableStatus.getAccount().getUsername());
Status.Visibility replyVisibility = actionableStatus.getVisibility(); for (Status.Mention mention : mentions) {
String contentWarning = actionableStatus.getSpoilerText(); mentionedUsernames.add(mention.getUsername());
Status.Mention[] mentions = actionableStatus.getMentions();
List<String> mentionedUsernames = new ArrayList<>();
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
for (Status.Mention mention : mentions) {
mentionedUsernames.add(mention.getUsername());
}
mentionedUsernames.removeAll(Collections.singleton(account.getUsername()));
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
replyIntent.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
.putExtra(KEY_CITED_TEXT, citedText)
.putExtra(KEY_CITED_STATUS_ID, inReplyToId)
.putExtra(KEY_VISIBILITY, replyVisibility)
.putExtra(KEY_SPOILER, contentWarning)
.putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0]));
} }
mentionedUsernames.removeAll(Collections.singleton(account.getUsername()));
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
.setAction(action)
.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
.putExtra(KEY_CITED_TEXT, citedText)
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
.putExtra(KEY_NOTIFICATION_ID, notificationId)
.putExtra(KEY_CITED_STATUS_ID, inReplyToId)
.putExtra(KEY_VISIBILITY, replyVisibility)
.putExtra(KEY_SPOILER, contentWarning)
.putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0]));
return PendingIntent.getBroadcast(context.getApplicationContext(), return PendingIntent.getBroadcast(context.getApplicationContext(),
notificationId, notificationId,
@ -400,8 +374,7 @@ public class NotificationHelper {
CHANNEL_BOOST + account.getIdentifier(), CHANNEL_BOOST + account.getIdentifier(),
CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(),
CHANNEL_EMOJI_REACTION + account.getIdentifier(), CHANNEL_EMOJI_REACTION + account.getIdentifier()
CHANNEL_CHAT_MESSAGES + account.getIdentifier()
}; };
int[] channelNames = { int[] channelNames = {
R.string.notification_mention_name, R.string.notification_mention_name,
@ -411,7 +384,6 @@ public class NotificationHelper {
R.string.notification_favourite_name, R.string.notification_favourite_name,
R.string.notification_poll_name, R.string.notification_poll_name,
R.string.notification_emoji_name, R.string.notification_emoji_name,
R.string.notification_chat_message_name,
}; };
int[] channelDescriptions = { int[] channelDescriptions = {
R.string.notification_mention_descriptions, R.string.notification_mention_descriptions,
@ -420,8 +392,7 @@ public class NotificationHelper {
R.string.notification_boost_description, R.string.notification_boost_description,
R.string.notification_favourite_description, R.string.notification_favourite_description,
R.string.notification_poll_description, R.string.notification_poll_description,
R.string.notification_emoji_description, R.string.notification_emoji_description
R.string.notification_chat_message_description,
}; };
List<NotificationChannel> channels = new ArrayList<>(6); List<NotificationChannel> channels = new ArrayList<>(6);
@ -579,8 +550,6 @@ public class NotificationHelper {
return account.getNotificationsPolls(); return account.getNotificationsPolls();
case EMOJI_REACTION: case EMOJI_REACTION:
return account.getNotificationsEmojiReactions(); return account.getNotificationsEmojiReactions();
case CHAT_MESSAGE:
return account.getNotificationsChatMessages();
default: default:
return false; return false;
} }
@ -603,8 +572,6 @@ public class NotificationHelper {
return CHANNEL_POLL + account.getIdentifier(); return CHANNEL_POLL + account.getIdentifier();
case EMOJI_REACTION: case EMOJI_REACTION:
return CHANNEL_EMOJI_REACTION + account.getIdentifier(); return CHANNEL_EMOJI_REACTION + account.getIdentifier();
case CHAT_MESSAGE:
return CHANNEL_CHAT_MESSAGES + account.getIdentifier();
default: default:
return null; return null;
} }
@ -686,9 +653,6 @@ public class NotificationHelper {
} else { } else {
return context.getString(R.string.poll_ended_voted); return context.getString(R.string.poll_ended_voted);
} }
case CHAT_MESSAGE:
return String.format(context.getString(R.string.notification_chat_message_format),
accountName);
} }
return null; return null;
} }
@ -716,22 +680,12 @@ public class NotificationHelper {
Poll poll = notification.getStatus().getPoll(); Poll poll = notification.getStatus().getPoll();
for(PollOption option: poll.getOptions()) { for(PollOption option: poll.getOptions()) {
builder.append(buildDescription(option.getTitle(), builder.append(buildDescription(option.getTitle(),
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()), PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotesCount()),
context)); context));
builder.append('\n'); builder.append('\n');
} }
return builder.toString(); return builder.toString();
} }
case CHAT_MESSAGE:
if (!TextUtils.isEmpty(notification.getChatMessage().getContent())) {
return notification.getChatMessage().getContent().toString();
} else if(notification.getChatMessage().getAttachment() != null) {
return context.getString(notification.getChatMessage().getAttachment().describeAttachmentType());
} else if(notification.getChatMessage().getCard() != null) {
return context.getString(R.string.link);
} else {
return "";
}
} }
return null; return null;
} }

View File

@ -16,35 +16,83 @@
package com.keylesspalace.tusky.components.notifications package com.keylesspalace.tusky.components.notifications
import android.content.Context import android.content.Context
import android.util.Log
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerFactory import androidx.work.WorkerFactory
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isLessThan
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class NotificationWorker( class NotificationWorker(
context: Context, private val context: Context,
params: WorkerParameters, params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher private val mastodonApi: MastodonApi,
private val accountManager: AccountManager
) : Worker(context, params) { ) : Worker(context, params) {
override fun doWork(): Result { override fun doWork(): Result {
notificationsFetcher.fetchAndShow() val accountList = accountManager.getAllAccountsOrderedByActive()
for (account in accountList) {
if (account.notificationsEnabled) {
try {
Log.d(TAG, "getting Notifications for " + account.fullName)
// don't care about withMuted because they are always silently ignored
val notificationsResponse = mastodonApi.notificationsWithAuth(
String.format("Bearer %s", account.accessToken),
account.domain, true
).execute()
val notifications = notificationsResponse.body()
if (notificationsResponse.isSuccessful && notifications != null) {
onNotificationsReceived(account, notifications)
} else {
Log.w(TAG, "error receiving notifications")
}
} catch (e: IOException) {
Log.w(TAG, "error receiving notifications", e)
}
}
}
return Result.success() return Result.success()
} }
private fun onNotificationsReceived(account: AccountEntity, notificationList: List<Notification>) {
val newId = account.lastNotificationId
var newestId = ""
var isFirstOfBatch = true
notificationList.reversed().forEach { notification ->
val currentId = notification.id
if (newestId.isLessThan(currentId)) {
newestId = currentId
}
if (newId.isLessThan(currentId)) {
NotificationHelper.make(context, notification, account, isFirstOfBatch)
isFirstOfBatch = false
}
}
account.lastNotificationId = newestId
accountManager.saveAccount(account)
}
companion object {
private const val TAG = "NotificationWorker"
}
} }
class NotificationWorkerFactory @Inject constructor( class NotificationWorkerFactory @Inject constructor(
private val notificationsFetcher: NotificationFetcher val api: MastodonApi,
) : WorkerFactory() { val accountManager: AccountManager
): WorkerFactory() {
override fun createWorker( override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
appContext: Context, if(workerClassName == NotificationWorker::class.java.name) {
workerClassName: String, return NotificationWorker(appContext, workerParameters, api, accountManager)
workerParameters: WorkerParameters
): ListenableWorker? {
if (workerClassName == NotificationWorker::class.java.name) {
return NotificationWorker(appContext, workerParameters, notificationsFetcher)
} }
return null return null
} }

View File

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

View File

@ -1,258 +0,0 @@
package com.keylesspalace.tusky.components.preference
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import okhttp3.OkHttpClient
import kotlin.system.exitProcess
/**
* This Preference lets the user select their preferred emoji font
*/
class EmojiPreference(
context: Context,
private val okHttpClient: OkHttpClient
) : Preference(context) {
private lateinit var selected: EmojiCompatFont
private lateinit var original: EmojiCompatFont
private val radioButtons = mutableListOf<RadioButton>()
private var updated = false
private var currentNeedsUpdate = false
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null }
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
super.onAttachedToHierarchy(preferenceManager)
// Find out which font is currently active
selected = EmojiCompatFont.byId(
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
)
// We'll use this later to determine if anything has changed
original = selected
summary = selected.getDisplay(context)
}
override fun onClick() {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null)
viewIds.forEachIndexed { index, viewId ->
setupItem(view.findViewById(viewId), FONTS[index])
}
AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun setupItem(container: View, font: EmojiCompatFont) {
val title: TextView = container.findViewById(R.id.emojicompat_name)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb)
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
// Initialize all the views
title.text = font.getDisplay(container.context)
caption.setText(font.caption)
thumb.setImageResource(font.img)
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
radioButtons.add(radio)
updateItem(font, container)
// Set actions
download.setOnClickListener { startDownload(font, container) }
cancel.setOnClickListener { cancelDownload(font, container) }
radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
container.setOnClickListener { containerView: View ->
select(font, containerView.findViewById(R.id.emojicompat_radio))
}
}
private fun startDownload(font: EmojiCompatFont, container: View) {
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
// Switch to downloading style
download.visibility = View.GONE
caption.visibility = View.INVISIBLE
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
cancel.visibility = View.VISIBLE
font.downloadFontFile(context, okHttpClient)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) {
progressBar.isIndeterminate = false
val max = progressBar.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress((max * progress).toInt(), true)
} else {
progressBar.progress = (max * progress).toInt()
}
} else {
progressBar.isIndeterminate = true
}
},
{
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
updateItem(font, container)
},
{
finishDownload(font, container)
}
).also { downloadDisposables[font.id] = it }
}
private fun cancelDownload(font: EmojiCompatFont, container: View) {
font.deleteDownloadedFile(container.context)
downloadDisposables[font.id]?.dispose()
downloadDisposables[font.id] = null
updateItem(font, container)
}
private fun finishDownload(font: EmojiCompatFont, container: View) {
select(font, container.findViewById(R.id.emojicompat_radio))
updateItem(font, container)
// Set the flag to restart the app (because an update has been downloaded)
if (selected === original && currentNeedsUpdate) {
updated = true
currentNeedsUpdate = false
}
}
/**
* Select a font both visually and logically
*
* @param font The font to be selected
* @param radio The radio button associated with it's visual item
*/
private fun select(font: EmojiCompatFont, radio: RadioButton) {
selected = font
// Uncheck all the other buttons
for (other in radioButtons) {
if (other !== radio) {
other.isChecked = false
}
}
radio.isChecked = true
}
/**
* Called when a "consistent" state is reached, i.e. it's not downloading the font
*
* @param font The font to be displayed
* @param container The ConstraintLayout containing the item
*/
private fun updateItem(font: EmojiCompatFont, container: View) {
// Assignments
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
// There's no download going on
progress.visibility = View.GONE
cancel.visibility = View.GONE
caption.visibility = View.VISIBLE
if (font.isDownloaded(context)) {
// Make it selectable
download.visibility = View.GONE
radio.visibility = View.VISIBLE
container.isClickable = true
} else {
// Make it downloadable
download.visibility = View.VISIBLE
radio.visibility = View.GONE
container.isClickable = false
}
// Select it if necessary
if (font === selected) {
radio.isChecked = true
// Update available
if (!font.isDownloaded(context)) {
currentNeedsUpdate = true
}
} else {
radio.isChecked = false
}
}
private fun saveSelectedFont() {
val index = selected.id
Log.i(TAG, "saveSelectedFont: Font ID: $index")
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putInt(key, index)
.apply()
summary = selected.getDisplay(context)
}
/**
* User clicked ok -> save the selected font and offer to restart the app if something changed
*/
private fun onDialogOk() {
saveSelectedFont()
if (selected !== original || updated) {
AlertDialog.Builder(context)
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent)
exitProcess(0)
}.show()
}
}
companion object {
private const val TAG = "EmojiPreference"
// Please note that this array must sorted in the same way as the fonts.
private val viewIds = intArrayOf(
R.id.item_nomoji,
R.id.item_blobmoji,
R.id.item_twemoji,
R.id.item_notoemoji
)
}
}

View File

@ -19,9 +19,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.paging.PagedList import androidx.paging.PagedList
import com.keylesspalace.tusky.appstore.BlockEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MuteEvent
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
@ -34,7 +31,6 @@ import javax.inject.Inject
class ReportViewModel @Inject constructor( class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val eventHub: EventHub,
private val statusesRepository: StatusesRepository) : RxAwareViewModel() { private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
private val navigationMutable = MutableLiveData<Screen>() private val navigationMutable = MutableLiveData<Screen>()
@ -127,8 +123,7 @@ class ReportViewModel @Inject constructor(
} }
fun toggleMute() { fun toggleMute() {
val alreadyMuted = muteStateMutable.value?.data == true if (muteStateMutable.value?.data == true) {
if (alreadyMuted) {
mastodonApi.unmuteAccountObservable(accountId) mastodonApi.unmuteAccountObservable(accountId)
} else { } else {
mastodonApi.muteAccountObservable(accountId) mastodonApi.muteAccountObservable(accountId)
@ -137,11 +132,7 @@ class ReportViewModel @Inject constructor(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ relationship -> { relationship ->
val muting = relationship?.muting == true muteStateMutable.value = Success(relationship?.muting == true)
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId))
}
}, },
{ error -> { error ->
muteStateMutable.value = Error(false, error.message) muteStateMutable.value = Error(false, error.message)
@ -152,8 +143,7 @@ class ReportViewModel @Inject constructor(
} }
fun toggleBlock() { fun toggleBlock() {
val alreadyBlocked = blockStateMutable.value?.data == true if (blockStateMutable.value?.data == true) {
if (alreadyBlocked) {
mastodonApi.unblockAccountObservable(accountId) mastodonApi.unblockAccountObservable(accountId)
} else { } else {
mastodonApi.blockAccountObservable(accountId) mastodonApi.blockAccountObservable(accountId)
@ -162,11 +152,7 @@ class ReportViewModel @Inject constructor(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ relationship -> { relationship ->
val blocking = relationship?.blocking == true blockStateMutable.value = Success(relationship?.blocking == true)
blockStateMutable.value = Success(blocking)
if (blocking) {
eventHub.dispatch(BlockEvent(accountId))
}
}, },
{ error -> { error ->
blockStateMutable.value = Error(false, error.message) blockStateMutable.value = Error(false, error.message)

View File

@ -101,6 +101,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
snackbarErrorRetry?.dismiss() snackbarErrorRetry?.dismiss()

View File

@ -59,6 +59,8 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(this, android.R.attr.colorBackground))
scheduledTootList.setHasFixedSize(true) scheduledTootList.setHasFixedSize(true)
scheduledTootList.layoutManager = LinearLayoutManager(this) scheduledTootList.layoutManager = LinearLayoutManager(this)

View File

@ -58,6 +58,9 @@ abstract class SearchFragment<T> : Fragment(),
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setOnRefreshListener(this) swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground)
)
} }
private fun subscribeObservables() { private fun subscribeObservables() {

View File

@ -155,13 +155,6 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
} }
override fun onViewReplyTo(position: Int) {
searchAdapter.getItem(position)?.first?.let { status ->
val actionableStatus = status.actionableStatus
bottomSheetActivity?.viewThread(actionableStatus.inReplyToId!!, null)
}
}
override fun onOpenReblog(position: Int) { override fun onOpenReblog(position: Int) {
searchAdapter.getItem(position)?.first?.let { status -> searchAdapter.getItem(position)?.first?.let { status ->
bottomSheetActivity?.viewAccount(status.account.id) bottomSheetActivity?.viewAccount(status.account.id)
@ -202,6 +195,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
} }
private fun onShowReplyTo(replyToId: String) {
bottomSheetActivity?.viewThread(replyToId, null)
}
companion object { companion object {
fun newInstance() = SearchStatusesFragment() fun newInstance() = SearchStatusesFragment()
} }
@ -275,6 +272,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
openAsItem.title = openAsTitle openAsItem.title = openAsTitle
if(status.inReplyToId == null) {
popup.menu.findItem(R.id.status_reply_to)?.isVisible = false
}
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.status_share_content -> { R.id.status_share_content -> {
@ -308,6 +309,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), context); LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), context);
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_reply_to -> {
onShowReplyTo(status.inReplyToId!!)
return@setOnMenuItemClickListener true
}
R.id.status_open_as -> { R.id.status_open_as -> {
showOpenAsDialog(statusUrl!!, item.title) showOpenAsDialog(statusUrl!!, item.title)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true

Some files were not shown because too many files have changed in this diff Show More