Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-04-09 00:34:31 +09:00
commit 1370eedc10
114 changed files with 2806 additions and 755 deletions

View File

@ -103,11 +103,11 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
} }
ext.lifecycleVersion = "2.2.0" ext.lifecycleVersion = "2.2.0"
ext.roomVersion = '2.2.4' ext.roomVersion = '2.2.5'
ext.retrofitVersion = '2.7.1' ext.retrofitVersion = '2.8.1'
ext.okhttpVersion = '4.3.1' ext.okhttpVersion = '4.4.0'
ext.glideVersion = '4.10.0' ext.glideVersion = '4.11.0'
ext.daggerVersion = '2.26' ext.daggerVersion = '2.27'
repositories { repositories {
maven { maven {
@ -120,12 +120,12 @@ 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.2.0" implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.appcompat:appcompat:1.2.0-alpha02" implementation "androidx.appcompat:appcompat:1.2.0-beta01"
implementation "androidx.fragment:fragment-ktx:1.2.2" implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.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.1.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.0" implementation "androidx.preference:preference:1.1.0"
implementation "androidx.sharetarget:sharetarget:1.0.0-rc01" implementation "androidx.sharetarget:sharetarget:1.0.0-rc01"
@ -135,7 +135,7 @@ dependencies {
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.2"
implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion"
@ -150,7 +150,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
implementation "org.conscrypt:conscrypt-android:2.2.1" implementation "org.conscrypt:conscrypt-android:2.4.0"
implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
@ -168,7 +168,7 @@ dependencies {
implementation "com.google.dagger:dagger-android-support:$daggerVersion" implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion" kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
implementation "com.github.connyduck:sparkbutton:3.0.0" implementation "com.github.connyduck:sparkbutton:4.0.0"
implementation "com.github.chrisbanes:PhotoView:2.3.0" implementation "com.github.chrisbanes:PhotoView:2.3.0"

View File

@ -0,0 +1,735 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "eaa3c4d012fe743948343983fe1ae493",
"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)",
"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
}
],
"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, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` 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
}
],
"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": []
}
],
"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, 'eaa3c4d012fe743948343983fe1ae493')"
]
}
}

View File

@ -0,0 +1,741 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "03a7436643ef356198742c5f8e054f5f",
"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)",
"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
}
],
"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, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, 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": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"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": []
}
],
"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, '03a7436643ef356198742c5f8e054f5f')"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -52,8 +52,6 @@ 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
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
@ -311,7 +309,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Subscribe to data loaded at the view model * Subscribe to data loaded at the view model
*/ */
private fun subscribeObservables() { private fun subscribeObservables() {
viewModel.accountData.observe(this, Observer<Resource<Account>> { viewModel.accountData.observe(this, Observer {
when (it) { when (it) {
is Success -> onAccountChanged(it.data) is Success -> onAccountChanged(it.data)
is Error -> { is Error -> {
@ -321,7 +319,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} }
}) })
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> { viewModel.relationshipData.observe(this, Observer {
val relation = it?.data val relation = it?.data
if (relation != null) { if (relation != null) {
onRelationshipChanged(relation) onRelationshipChanged(relation)
@ -334,7 +332,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
}) })
viewModel.accountFieldData.observe(this, Observer<List<Either<IdentityProof, Field>>> { viewModel.accountFieldData.observe(this, Observer {
accountFieldAdapter.fields = it accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
@ -681,6 +679,30 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.show() .show()
} }
private fun toggleBlock() {
if (viewModel.relationshipData.value?.data?.blocking != true) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
} else {
viewModel.changeBlockState()
}
}
private fun toggleMute() {
if (viewModel.relationshipData.value?.data?.muting != true) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_mute_warning, loadedAccount?.username))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeMuteState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
} else {
viewModel.changeMuteState()
}
}
private fun mention() { private fun mention() {
loadedAccount?.let { loadedAccount?.let {
val intent = ComposeActivity.startIntent(this, val intent = ComposeActivity.startIntent(this,
@ -727,11 +749,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return true return true
} }
R.id.action_block -> { R.id.action_block -> {
viewModel.changeBlockState() toggleBlock()
return true return true
} }
R.id.action_mute -> { R.id.action_mute -> {
viewModel.changeMuteState() toggleMute()
return true return true
} }
R.id.action_mute_domain -> { R.id.action_mute_domain -> {

View File

@ -77,7 +77,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java) viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
val args = arguments!! val args = requireArguments()
listId = args.getString(LIST_ID_ARG)!! listId = args.getString(LIST_ID_ARG)!!
listName = args.getString(LIST_NAME_ARG)!! listName = args.getString(LIST_NAME_ARG)!!
@ -255,12 +255,12 @@ class AccountsInListFragment : DialogFragment(), Injectable {
loadAvatar(account.avatar, avatar, radius, animateAvatar) loadAvatar(account.avatar, avatar, radius, animateAvatar)
rejectButton.apply { rejectButton.apply {
if (inAList) { contentDescription = if (inAList) {
setImageResource(R.drawable.ic_reject_24dp) setImageResource(R.drawable.ic_reject_24dp)
contentDescription = getString(R.string.action_remove_from_list) getString(R.string.action_remove_from_list)
} else { } else {
setImageResource(R.drawable.ic_plus_24dp) setImageResource(R.drawable.ic_plus_24dp)
contentDescription = getString(R.string.action_add_to_list) getString(R.string.action_add_to_list)
} }
} }
} }

View File

@ -135,7 +135,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
editText.onTextChanged { s, _, _, _ -> editText.onTextChanged { s, _, _, _ ->
positiveButton.isEnabled = !s.isNullOrBlank() positiveButton.isEnabled = !s.isBlank()
} }
editText.setText(list?.title) editText.setText(list?.title)
editText.text?.let { editText.setSelection(it.length) } editText.text?.let { editText.setSelection(it.length) }

View File

@ -129,7 +129,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "viewPagerOffScreenLimit" -> { "useBlurhash", "showCardsInTimelines", "confirmReblogs", "viewPagerOffScreenLimit" -> {
restartActivitiesOnExit = true restartActivitiesOnExit = true
} }
"language" -> { "language" -> {

View File

@ -277,7 +277,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
addTabAdapter.updateData(addableTabs) addTabAdapter.updateData(addableTabs)
maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT); currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT)
} }
override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) { override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) {

View File

@ -0,0 +1,51 @@
package com.keylesspalace.tusky.adapter
import android.view.View
import androidx.core.text.BidiFormatter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.visible
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
private var id: String? = null
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
.getBoolean("animateGifAvatars", false)
fun setupWithAccount(account: Account, formatter: BidiFormatter?) {
id = account.id
val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name
val emojifiedName: CharSequence = CustomEmojiHelper.emojifyString(wrappedName, account.emojis, itemView)
itemView.displayNameTextView.text = emojifiedName
if (showHeader) {
itemView.notificationTextView?.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName)
}
itemView.notificationTextView?.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format)
val formattedUsername = String.format(format, account.username)
itemView.usernameTextView.text = formattedUsername
val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar)
}
fun setupActionListener(listener: AccountActionListener) {
itemView.acceptButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position)
}
}
itemView.rejectButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(false, id, position)
}
}
itemView.avatar.setOnClickListener { listener.onViewAccount(id) }
}
}

View File

@ -18,19 +18,12 @@ package com.keylesspalace.tusky.adapter;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class FollowRequestsAdapter extends AccountAdapter { public class FollowRequestsAdapter extends AccountAdapter {
@ -46,7 +39,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
case VIEW_TYPE_ACCOUNT: { case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_follow_request, parent, false); .inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view); return new FollowRequestViewHolder(view, false);
} }
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
@ -60,57 +53,8 @@ public class FollowRequestsAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), null);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
static class FollowRequestViewHolder extends RecyclerView.ViewHolder {
private ImageView avatar;
private TextView username;
private TextView displayName;
private ImageButton accept;
private ImageButton reject;
private String id;
private boolean animateAvatar;
FollowRequestViewHolder(View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.avatar);
username = itemView.findViewById(R.id.usernameTextView);
displayName = itemView.findViewById(R.id.displayNameTextView);
accept = itemView.findViewById(R.id.acceptButton);
reject = itemView.findViewById(R.id.rejectButton);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
}
void setupWithAccount(Account account) {
id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), account.getEmojis(), displayName);
displayName.setText(emojifiedName);
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupActionListener(final AccountActionListener listener) {
accept.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position);
}
});
reject.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(false, id, position);
}
});
avatar.setOnClickListener(v -> listener.onViewAccount(id));
}
}
} }

View File

@ -44,8 +44,10 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
@ -75,8 +77,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS = 0; private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1; private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
private static final int VIEW_TYPE_FOLLOW = 2; private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_PLACEHOLDER = 3; private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
private static final int VIEW_TYPE_UNKNOWN = 4; private static final int VIEW_TYPE_PLACEHOLDER = 4;
private static final int VIEW_TYPE_UNKNOWN = 5;
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
@ -85,6 +88,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusDisplayOptions statusDisplayOptions; private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener; private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener; private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
private BidiFormatter bidiFormatter; private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource; private AdapterDataSource<NotificationViewData> dataSource;
@ -92,13 +96,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
AdapterDataSource<NotificationViewData> dataSource, AdapterDataSource<NotificationViewData> dataSource,
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener, StatusActionListener statusListener,
NotificationActionListener notificationActionListener) { NotificationActionListener notificationActionListener,
AccountActionListener accountActionListener) {
this.accountId = accountId; this.accountId = accountId;
this.dataSource = dataSource; this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions; this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener; this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener; this.notificationActionListener = notificationActionListener;
this.accountActionListener = accountActionListener;
bidiFormatter = BidiFormatter.getInstance(); bidiFormatter = BidiFormatter.getInstance();
} }
@ -122,6 +128,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_follow, parent, false); .inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view, statusDisplayOptions); return new FollowViewHolder(view, statusDisplayOptions);
} }
case VIEW_TYPE_FOLLOW_REQUEST: {
View view = inflater
.inflate(R.layout.item_follow_request_notification, parent, false);
return new FollowRequestViewHolder(view, true);
}
case VIEW_TYPE_PLACEHOLDER: { case VIEW_TYPE_PLACEHOLDER: {
View view = inflater View view = inflater
.inflate(R.layout.item_status_placeholder, parent, false); .inflate(R.layout.item_status_placeholder, parent, false);
@ -218,6 +229,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
break; break;
} }
case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
holder.setupActionListener(accountActionListener);
}
}
default: default:
} }
} }
@ -234,7 +252,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
mediaPreviewEnabled, mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash() statusDisplayOptions.useBlurhash(),
CardViewMode.NONE,
statusDisplayOptions.confirmReblogs()
); );
} }
@ -259,6 +279,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW: { case FOLLOW: {
return VIEW_TYPE_FOLLOW; return VIEW_TYPE_FOLLOW;
} }
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
default: { default: {
return VIEW_TYPE_UNKNOWN; return VIEW_TYPE_UNKNOWN;
} }

View File

@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.util.CustomEmojiHelper import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.HtmlUtils
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.PollOptionViewData import com.keylesspalace.tusky.viewdata.PollOptionViewData
import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.buildDescription
@ -36,12 +35,19 @@ 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()
fun setup(options: List<PollOptionViewData>, voteCount: Int, emojis: List<Emoji>, mode: Int) { fun setup(
options: List<PollOptionViewData>,
voteCount: Int,
votersCount: Int?,
emojis: List<Emoji>,
mode: Int) {
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
notifyDataSetChanged() notifyDataSetChanged()
@ -71,7 +77,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
when(mode) { when(mode) {
RESULT -> { RESULT -> {
val percent = calculatePercent(option.votesCount, voteCount) val percent = calculatePercent(option.votesCount, votersCount, voteCount)
val emojifiedPollOptionText = CustomEmojiHelper.emojifyText(buildDescription(option.title, percent, holder.resultTextView.context), emojis, holder.resultTextView) val emojifiedPollOptionText = CustomEmojiHelper.emojifyText(buildDescription(option.title, percent, holder.resultTextView.context), emojis, holder.resultTextView)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)

View File

@ -8,31 +8,38 @@ import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -92,6 +99,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private TextView pollDescription; private TextView pollDescription;
private Button pollButton; private Button pollButton;
private LinearLayout cardView;
private LinearLayout cardInfo;
private ImageView cardImage;
private TextView cardTitle;
private TextView cardDescription;
private TextView cardUrl;
private PollAdapter pollAdapter; private PollAdapter pollAdapter;
private SimpleDateFormat shortSdf; private SimpleDateFormat shortSdf;
@ -152,6 +165,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollDescription = itemView.findViewById(R.id.status_poll_description); pollDescription = itemView.findViewById(R.id.status_poll_description);
pollButton = itemView.findViewById(R.id.status_poll_button); pollButton = itemView.findViewById(R.id.status_poll_button);
cardView = itemView.findViewById(R.id.status_card_view);
cardInfo = itemView.findViewById(R.id.card_info);
cardImage = itemView.findViewById(R.id.card_image);
cardTitle = itemView.findViewById(R.id.card_title);
cardDescription = itemView.findViewById(R.id.card_description);
cardUrl = itemView.findViewById(R.id.card_link);
pollAdapter = new PollAdapter(); pollAdapter = new PollAdapter();
pollOptions.setAdapter(pollAdapter); pollOptions.setAdapter(pollAdapter);
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
@ -200,7 +220,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
contentWarningDescription.setVisibility(View.VISIBLE); contentWarningDescription.setVisibility(View.VISIBLE);
contentWarningButton.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE);
setContentWarningButtonText(expanded); setContentWarningButtonText(expanded);
contentWarningButton.setOnClickListener( view -> { contentWarningButton.setOnClickListener(view -> {
contentWarningDescription.invalidate(); contentWarningDescription.invalidate();
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(!expanded, getAdapterPosition()); listener.onExpandedChange(!expanded, getAdapterPosition());
@ -218,7 +238,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
private void setContentWarningButtonText(boolean expanded) { private void setContentWarningButtonText(boolean expanded) {
if(expanded) { if (expanded) {
contentWarningButton.setText(R.string.status_content_warning_show_less); contentWarningButton.setText(R.string.status_content_warning_show_less);
} else { } else {
contentWarningButton.setText(R.string.status_content_warning_show_more); contentWarningButton.setText(R.string.status_content_warning_show_more);
@ -673,8 +693,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setVisibility(View.GONE); sensitiveMediaShow.setVisibility(View.GONE);
} }
protected void setupButtons(final StatusActionListener listener, final String accountId, protected void setupButtons(final StatusActionListener listener,
final boolean isNotestock, final String acct) { final String accountId,
final String statusContent,
final boolean isNotestock,
final String acct,
StatusDisplayOptions statusDisplayOptions) {
avatar.setOnClickListener(v -> { avatar.setOnClickListener(v -> {
if (isNotestock) { if (isNotestock) {
@ -702,9 +726,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
replyButton.setClickable(!isNotestock); replyButton.setClickable(!isNotestock);
if (reblogButton != null) { if (reblogButton != null) {
reblogButton.setEventListener((button, buttonState) -> { reblogButton.setEventListener((button, buttonState) -> {
// return true to play animaion
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onReblog(buttonState, position); if (statusDisplayOptions.confirmReblogs()) {
showConfirmReblogDialog(listener, statusContent, buttonState, position);
return false;
} else {
listener.onReblog(!buttonState, position);
return true;
}
} else {
return false;
} }
}); });
} }
@ -712,8 +745,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
favouriteButton.setEventListener((button, buttonState) -> { favouriteButton.setEventListener((button, buttonState) -> {
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onFavourite(buttonState, position); listener.onFavourite(!buttonState, position);
} }
return true;
}); });
favouriteButton.setEnabled(!isNotestock); favouriteButton.setEnabled(!isNotestock);
@ -731,8 +765,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton.setEventListener((button, buttonState) -> { bookmarkButton.setEventListener((button, buttonState) -> {
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onBookmark(buttonState, position); listener.onBookmark(!buttonState, position);
} }
return true;
}); });
moreButton.setOnClickListener(v -> { moreButton.setOnClickListener(v -> {
@ -757,6 +792,23 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
itemView.setOnClickListener(viewThreadListener); itemView.setOnClickListener(viewThreadListener);
} }
private void showConfirmReblogDialog(StatusActionListener listener,
String statusContent,
boolean buttonState,
int position) {
int okButtonTextId = buttonState ? R.string.action_unreblog : R.string.action_reblog;
new AlertDialog.Builder(reblogButton.getContext())
.setMessage(statusContent)
.setPositiveButton(okButtonTextId, (__, ___) -> {
listener.onReblog(!buttonState, position);
if (!buttonState) {
// Play animation only when it's reblog, not unreblog
reblogButton.playAnimation();
}
})
.show();
}
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions) { StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, statusDisplayOptions, null); this.setupWithStatus(status, listener, statusDisplayOptions, null);
@ -799,8 +851,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
} }
setupButtons(listener, status.getSenderId(), status.isNotestock(), status.getNickname()); if (cardView != null) {
setRebloggingEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility()); setupCard(status, statusDisplayOptions.cardViewMode());
}
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
status.isNotestock(), status.getNickname(), statusDisplayOptions);
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility()); setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener, status.getQuote() != null); setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener, status.getQuote() != null);
@ -934,7 +991,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.getVotesCount()); int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), 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] = "";
@ -949,7 +1006,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getFavsText(Context context, int count) { protected CharSequence getFavsText(Context context, int count) {
if (count > 0) { if (count > 0) {
String countString = numberFormat.format(count); String countString = numberFormat.format(count);
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString)); return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else { } else {
return ""; return "";
} }
@ -958,7 +1015,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getReblogsText(Context context, int count) { protected CharSequence getReblogsText(Context context, int count) {
if (count > 0) { if (count > 0) {
String countString = numberFormat.format(count); String countString = numberFormat.format(count);
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString)); return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else { } else {
return ""; return "";
} }
@ -977,12 +1034,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (expired || poll.getVoted()) { if (expired || poll.getVoted()) {
// no voting possible // no voting possible
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT); pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT);
pollButton.setVisibility(View.GONE); pollButton.setVisibility(View.GONE);
} else { } else {
// voting possible // voting possible
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE); pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
pollButton.setVisibility(View.VISIBLE); pollButton.setVisibility(View.VISIBLE);
@ -1009,8 +1066,14 @@ 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);
@ -1028,6 +1091,80 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
} }
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode) {
if (cardViewMode != CardViewMode.NONE && status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) {
final Card card = status.getCard();
cardView.setVisibility(View.VISIBLE);
cardTitle.setText(card.getTitle());
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
cardDescription.setVisibility(View.GONE);
} else {
cardDescription.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(card.getDescription())) {
cardDescription.setText(card.getAuthorName());
} else {
cardDescription.setText(card.getDescription());
}
}
cardUrl.setText(card.getUrl());
if (!TextUtils.isEmpty(card.getImage())) {
int topLeftRadius = 0;
int topRightRadius = 0;
int bottomRightRadius = 0;
int bottomLeftRadius = 0;
int radius = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_radius);
if (card.getWidth() > card.getHeight()) {
cardView.setOrientation(LinearLayout.VERTICAL);
cardImage.getLayoutParams().height = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_vertical_height);
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
topLeftRadius = radius;
topRightRadius = radius;
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
topLeftRadius = radius;
bottomLeftRadius = radius;
}
Glide.with(cardImage)
.load(card.getImage())
.transform(
new CenterCrop(),
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
)
.into(cardImage);
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.setImageResource(R.drawable.card_image_placeholder);
}
cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext()));
cardView.setClipToOutline(true);
} else {
cardView.setVisibility(View.GONE);
}
}
private static String formatDuration(double durationInSeconds) { private static String formatDuration(double durationInSeconds) {
int seconds = (int) Math.round(durationInSeconds) % 60; int seconds = (int) Math.round(durationInSeconds) % 60;
int minutes = (int) durationInSeconds % 3600 / 60; int minutes = (int) durationInSeconds % 3600 / 60;

View File

@ -5,26 +5,19 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -37,27 +30,13 @@ import java.util.regex.Pattern;
class StatusDetailedViewHolder extends StatusBaseViewHolder { class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView reblogs; private TextView reblogs;
private TextView favourites; private TextView favourites;
private LinearLayout cardView;
private LinearLayout cardInfo;
private ImageView cardImage;
private TextView cardTitle;
private TextView cardDescription;
private TextView cardUrl;
private View infoDivider; private View infoDivider;
StatusDetailedViewHolder(View view) { StatusDetailedViewHolder(View view) {
super(view); super(view);
reblogs = view.findViewById(R.id.status_reblogs); reblogs = view.findViewById(R.id.status_reblogs);
favourites = view.findViewById(R.id.status_favourites); favourites = view.findViewById(R.id.status_favourites);
cardView = view.findViewById(R.id.card_view);
cardInfo = view.findViewById(R.id.card_info);
cardImage = view.findViewById(R.id.card_image);
cardTitle = view.findViewById(R.id.card_title);
cardDescription = view.findViewById(R.id.card_description);
cardUrl = view.findViewById(R.id.card_link);
infoDivider = view.findViewById(R.id.status_info_divider); infoDivider = view.findViewById(R.id.status_info_divider);
cardView.setClipToOutline(true);
} }
@Override @Override
@ -131,6 +110,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status
if (payloads == null) { if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
@ -149,97 +129,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
content.setOnLongClickListener(longClickListener); content.setOnLongClickListener(longClickListener);
contentWarningDescription.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener);
if (status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) {
final Card card = status.getCard();
cardView.setVisibility(View.VISIBLE);
cardTitle.setText(card.getTitle());
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
cardDescription.setVisibility(View.GONE);
} else {
cardDescription.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(card.getDescription())) {
cardDescription.setText(card.getAuthorName());
} else {
cardDescription.setText(card.getDescription());
}
}
cardUrl.setText(card.getUrl());
if (!TextUtils.isEmpty(card.getImage())) {
int topLeftRadius = 0;
int topRightRadius = 0;
int bottomRightRadius = 0;
int bottomLeftRadius = 0;
int radius = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_radius);
if (card.getWidth() > card.getHeight()) {
cardView.setOrientation(LinearLayout.VERTICAL);
cardImage.getLayoutParams().height = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_vertical_height);
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
topLeftRadius = radius;
topRightRadius = radius;
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
topLeftRadius = radius;
bottomLeftRadius = radius;
}
Glide.with(cardImage)
.load(card.getImage())
.transform(
new CenterCrop(),
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
)
.into(cardImage);
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
cardImage.setImageResource(R.drawable.card_image_placeholder);
}
cardView.setOnClickListener(v -> {
String url = card.getUrl();
String regex = ".*/users/[^/]+/statuses/([0-9]+)";
String replace = "$1";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(url);
if (m.find()) {
String id = m.replaceAll(replace);
Intent intent = new Intent(v.getContext(), ViewThreadActivity.class);
intent.putExtra("id", id);
intent.putExtra("url", url);
v.getContext().startActivity(intent);
} else {
LinkHelper.openLink(url, v.getContext());
}
});
} else {
cardView.setVisibility(View.GONE);
}
} }
} }
} }

View File

@ -63,7 +63,9 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
mediaPreviewEnabled, mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(), statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash() statusDisplayOptions.useBlurhash(),
statusDisplayOptions.cardViewMode(),
statusDisplayOptions.confirmReblogs()
); );
} }

View File

@ -8,6 +8,7 @@ import com.keylesspalace.tusky.entity.Status
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable
data class BookmarkEvent(val statusId: String, val bookmark: Boolean) : Dispatchable data class BookmarkEvent(val statusId: String, val bookmark: Boolean) : Dispatchable
data class MuteConversationEvent(val statusId: String, val mute: Boolean) : Dispatchable
data class UnfollowEvent(val accountId: String) : Dispatchable data class UnfollowEvent(val accountId: String) : Dispatchable
data class BlockEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable
data class MuteEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable

View File

@ -252,15 +252,14 @@ class ComposeActivity : BaseActivity(),
if (action != null && action == Intent.ACTION_SEND) { if (action != null && action == Intent.ACTION_SEND) {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
val text = intent.getStringExtra(Intent.EXTRA_TEXT) val text = intent.getStringExtra(Intent.EXTRA_TEXT)
val shareBody = if (subject != null && text != null) { val shareBody = text ?: subject
if (subject != text && !text.contains(subject)) {
String.format("%s\n%s", subject, text)
} else {
text
}
} else text ?: subject
if (shareBody != null) { if (shareBody != null) {
if (!subject.isNullOrBlank() && subject !in shareBody) {
composeContentWarningField.setText(subject)
viewModel.showContentWarning.value = true
}
val start = composeEditField.selectionStart.coerceAtLeast(0) val start = composeEditField.selectionStart.coerceAtLeast(0)
val end = composeEditField.selectionEnd.coerceAtLeast(0) val end = composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(start, end) val left = min(start, end)

View File

@ -271,7 +271,7 @@ class ComposeViewModel
text, text,
spoilerText, spoilerText,
statusVisibility.value!!.serverString(), statusVisibility.value!!.serverString(),
mediaUris.isNotEmpty() && markMediaAsSensitive.value!!, mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
mediaIds, mediaIds,
mediaUris.map { it.toString() }, mediaUris.map { it.toString() },
mediaDescriptions, mediaDescriptions,

View File

@ -18,26 +18,19 @@ package com.keylesspalace.tusky.components.compose.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.RadioGroup
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import kotlinx.android.synthetic.main.view_compose_options.view.* import kotlinx.android.synthetic.main.view_compose_options.view.*
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) {
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
var listener: ComposeOptionsListener? = null var listener: ComposeOptionsListener? = null
init { init {
inflate(context, R.layout.view_compose_options, this) inflate(context, R.layout.view_compose_options, this)
publicRadioButton.setButtonDrawable(R.drawable.ic_public_24dp) setOnCheckedChangeListener { _, checkedId ->
unlistedRadioButton.setButtonDrawable(R.drawable.ic_lock_open_24dp)
privateRadioButton.setButtonDrawable(R.drawable.ic_lock_outline_24dp)
unleakableRadioButton.setButtonDrawable(R.drawable.ic_low_vision_24dp)
directRadioButton.setButtonDrawable(R.drawable.ic_email_24dp)
visibilityRadioGroup.setOnCheckedChangeListener { _, checkedId ->
val visibility = when (checkedId) { val visibility = when (checkedId) {
R.id.publicRadioButton -> R.id.publicRadioButton ->
Status.Visibility.PUBLIC Status.Visibility.PUBLIC
@ -80,7 +73,7 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr
} }
visibilityRadioGroup.check(selectedButton) check(selectedButton)
} }
} }

View File

@ -192,6 +192,9 @@ public class ComposeScheduleView extends ConstraintLayout {
private void onDateSet(long selection) { private void onDateSet(long selection) {
initializeSuggestedTime(); initializeSuggestedTime();
Calendar newDate = getCalendar(); Calendar newDate = getCalendar();
// working around bug in DatePicker where date is UTC #1720
// see https://github.com/material-components/material-components-android/issues/882
newDate.setTimeZone(TimeZone.getTimeZone("UTC"));
newDate.setTimeInMillis(selection); newDate.setTimeInMillis(selection);
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
openPickTimeDialog(); openPickTimeDialog();

View File

@ -157,6 +157,7 @@ data class ConversationStatusEntity(
mentions = mentions, mentions = mentions,
application = null, application = null,
pinned = false, pinned = false,
muted = false,
poll = poll, poll = poll,
card = null, card = null,
quote = null) quote = null)

View File

@ -104,7 +104,8 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
} }
setupButtons(listener, account.getId(), false, account.getUsername()); setupButtons(listener, account.getId(), status.getContent().toString(),
false, account.getUsername(), statusDisplayOptions);
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
status.getMentions(), status.getEmojis(), status.getMentions(), status.getEmojis(),

View File

@ -36,10 +36,7 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.* import kotlinx.android.synthetic.main.fragment_timeline.*
import javax.inject.Inject import javax.inject.Inject
@ -68,7 +65,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", false)
) )

View File

@ -43,10 +43,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.* import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
@ -119,7 +116,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false, showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", false)
) )
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,

View File

@ -243,7 +243,7 @@ class SearchViewModel @Inject constructor(
return accountManager.getAllAccountsOrderedByActive() return accountManager.getAllAccountsOrderedByActive()
} }
fun muteAcount(accountId: String) { fun muteAccount(accountId: String) {
timelineCases.mute(accountId) timelineCases.mute(accountId)
} }
@ -263,6 +263,18 @@ class SearchViewModel @Inject constructor(
search(currentQuery) search(currentQuery)
} }
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
val idx = loadedStatuses.indexOf(status)
if (idx >= 0) {
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setMuted(mute).createStatusViewData())
loadedStatuses[idx] = newPair
repoResultStatus.value?.refresh?.invoke()
}
timelineCases.muteConversation(status.first, mute)
.onErrorReturnItem(status.first)
.subscribe()
.autoDispose()
}
companion object { companion object {
private const val TAG = "SearchViewModel" private const val TAG = "SearchViewModel"

View File

@ -49,8 +49,10 @@ import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
@ -70,7 +72,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
get() = viewModel.statuses get() = viewModel.statuses
private val searchAdapter private val searchAdapter
get() = super.adapter as SearchStatusesAdapter get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
@ -79,7 +81,9 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
mediaPreviewEnabled = viewModel.mediaPreviewEnabled, mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true) useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", false)
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
@ -250,12 +254,9 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
val loggedInAccountId = viewModel.activeAccount?.accountId val loggedInAccountId = viewModel.activeAccount?.accountId
val popup = PopupMenu(view.context, view) val popup = PopupMenu(view.context, view)
val statusIsByCurrentUser = loggedInAccountId?.equals(accountId) == true
// Give a different menu depending on whether this is the user's own toot or not. // Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || loggedInAccountId != accountId) { if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more)
val menu = popup.menu
menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty()
} else {
popup.inflate(R.menu.status_more_for_user) popup.inflate(R.menu.status_more_for_user)
val menu = popup.menu val menu = popup.menu
menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank() menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank()
@ -273,6 +274,10 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
Status.Visibility.UNKNOWN, Status.Visibility.UNLEAKABLE, Status.Visibility.DIRECT -> { Status.Visibility.UNKNOWN, Status.Visibility.UNLEAKABLE, Status.Visibility.DIRECT -> {
} //Ignore } //Ignore
} }
} else {
popup.inflate(R.menu.status_more)
val menu = popup.menu
menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty()
} }
val openAsItem = popup.menu.findItem(R.id.status_open_as) val openAsItem = popup.menu.findItem(R.id.status_open_as)
@ -288,6 +293,19 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
} }
openAsItem.title = openAsTitle openAsItem.title = openAsTitle
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
isVisible = mutable
}
if (mutable) {
muteConversationItem.setTitle(
if (status.muted == true) {
R.string.action_unmute_conversation
} else {
R.string.action_mute_conversation
})
}
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.status_share_content -> { R.id.status_share_content -> {
@ -325,12 +343,18 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
requestDownloadAllMedia(status) requestDownloadAllMedia(status)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_mute_conversation -> {
searchAdapter.getItem(position)?.let { foundStatus ->
viewModel.muteConversation(foundStatus, status.muted != true)
}
return@setOnMenuItemClickListener true
}
R.id.status_mute -> { R.id.status_mute -> {
viewModel.muteAcount(accountId) onMute(accountId, accountUsername)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_block -> { R.id.status_block -> {
viewModel.blockAccount(accountId) onBlock(accountId, accountUsername)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_report -> { R.id.status_report -> {
@ -363,6 +387,28 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
popup.show() popup.show()
} }
private fun onBlock(accountId: String, accountUsername: String) {
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun onMute(accountId: String, accountUsername: String) {
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_mute_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.muteAccount(accountId) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun accountIsInMentions(account: AccountEntity?, mentions: Array<Mention>): Boolean {
return mentions.firstOrNull {
account?.username == it.username && account.domain == Uri.parse(it.url)?.host
} != null
}
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) { private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener { bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) { override fun onAccountSelected(account: AccountEntity) {

View File

@ -39,6 +39,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var notificationsEnabled: Boolean = true, var notificationsEnabled: Boolean = true,
var notificationsMentioned: Boolean = true, var notificationsMentioned: Boolean = true,
var notificationsFollowed: Boolean = true, var notificationsFollowed: Boolean = true,
var notificationsFollowRequested: Boolean = false,
var notificationsReblogged: Boolean = true, var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true, var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true, var notificationsPolls: Boolean = true,
@ -54,7 +55,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var activeNotifications: String = "[]", var activeNotifications: String = "[]",
var emojis: List<Emoji> = emptyList(), var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(), var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[]") { var notificationsFilter: String = "[\"follow_request\"]") {
val identifier: String val identifier: String
get() = "$domain:$accountId" get() = "$domain:$accountId"

View File

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 21) }, version = 23)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); public abstract TootDao tootDao();
@ -326,4 +326,18 @@ public abstract class AppDatabase extends RoomDatabase {
} }
}; };
public static final Migration MIGRATION_21_22 = new Migration(21, 22) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0");
}
};
public static final Migration MIGRATION_22_23 = new Migration(22, 23) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER");
}
};
} }

View File

@ -16,6 +16,8 @@
package com.keylesspalace.tusky.db package com.keylesspalace.tusky.db
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -27,7 +29,6 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.HtmlUtils
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
@ -128,7 +129,7 @@ class Converters {
if(spanned == null) { if(spanned == null) {
return null return null
} }
return HtmlUtils.toHtml(spanned) return spanned.toHtml()
} }
@TypeConverter @TypeConverter
@ -136,7 +137,7 @@ class Converters {
if(spannedString == null) { if(spannedString == null) {
return null return null
} }
return HtmlUtils.fromHtml(spannedString) return spannedString.parseAsHtml()
} }
@TypeConverter @TypeConverter

View File

@ -51,7 +51,8 @@ data class TimelineStatusEntity(
val application: String?, val application: String?,
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
val reblogAccountId: String?, val reblogAccountId: String?,
val poll: String? val poll: String?,
val muted: Boolean?
) )
@Entity( @Entity(

View File

@ -29,8 +29,6 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.network.TimelineCasesImpl import com.keylesspalace.tusky.network.TimelineCasesImpl
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.HtmlConverterImpl
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Singleton import javax.inject.Singleton
@ -79,13 +77,9 @@ class AppModule {
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21) AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
.build() AppDatabase.MIGRATION_22_23)
.build()
} }
@Provides
@Singleton
fun providesHtmlConverter(): HtmlConverter {
return HtmlConverterImpl()
}
} }

View File

@ -6,17 +6,18 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRepository import com.keylesspalace.tusky.repository.TimelineRepository
import com.keylesspalace.tusky.repository.TimelineRepositoryImpl import com.keylesspalace.tusky.repository.TimelineRepositoryImpl
import com.keylesspalace.tusky.util.HtmlConverter
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@Module @Module
class RepositoryModule { class RepositoryModule {
@Provides @Provides
fun providesTimelineRepository(db: AppDatabase, mastodonApi: MastodonApi, fun providesTimelineRepository(
accountManager: AccountManager, gson: Gson, db: AppDatabase,
htmlConverter: HtmlConverter): TimelineRepository { mastodonApi: MastodonApi,
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson, accountManager: AccountManager,
htmlConverter) gson: Gson
): TimelineRepository {
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson)
} }
} }

View File

@ -15,24 +15,16 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import android.os.Parcel
import android.os.Parcelable
import android.text.Spanned import android.text.Spanned
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.keylesspalace.tusky.util.HtmlUtils import java.util.Date
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
import java.util.*
@Parcelize
data class Account( data class Account(
val id: String, val id: String,
@SerializedName("username") val localUsername: String, @SerializedName("username") val localUsername: String,
@SerializedName("acct", alternate = ["subject"]) val username: String, @SerializedName("acct", alternate = ["subject"]) val username: String,
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
val note: @WriteWith<SpannedParceler>() Spanned, val note: Spanned,
val url: String, val url: String,
val avatar: String, val avatar: String,
val header: String, val header: String,
@ -47,7 +39,7 @@ data class Account(
val moved: Account? = null, val moved: Account? = null,
@SerializedName("name") val notestockUsername: String? = null @SerializedName("name") val notestockUsername: String? = null
) : Parcelable { ) {
val name: String val name: String
get() = notestockUsername ?: if (displayName.isNullOrEmpty()) { get() = notestockUsername ?: if (displayName.isNullOrEmpty()) {
@ -87,31 +79,20 @@ data class Account(
fun isRemote(): Boolean = this.username != this.localUsername fun isRemote(): Boolean = this.username != this.localUsername
} }
@Parcelize
data class AccountSource( data class AccountSource(
val privacy: Status.Visibility, val privacy: Status.Visibility,
val sensitive: Boolean, val sensitive: Boolean,
val note: String, val note: String,
val fields: List<StringField>? val fields: List<StringField>?
): Parcelable )
@Parcelize
data class Field ( data class Field (
val name: String, val name: String,
val value: @WriteWith<SpannedParceler>() Spanned, val value: Spanned,
@SerializedName("verified_at") val verifiedAt: Date? @SerializedName("verified_at") val verifiedAt: Date?
): Parcelable )
@Parcelize
data class StringField ( data class StringField (
val name: String, val name: String,
val value: String val value: String
): Parcelable )
object SpannedParceler : Parceler<Spanned> {
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
override fun Spanned.write(parcel: Parcel, flags: Int) {
parcel.writeString(HtmlUtils.toHtml(this))
}
}

View File

@ -15,23 +15,19 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import android.os.Parcelable
import android.text.Spanned import android.text.Spanned
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
@Parcelize
data class Card( data class Card(
val url: String, val url: String,
val title: @WriteWith<SpannedParceler>() Spanned, val title: Spanned,
val description: @WriteWith<SpannedParceler>() Spanned, val description: Spanned,
@SerializedName("author_name") val authorName: String, @SerializedName("author_name") val authorName: String,
val image: String, val image: String,
val type: String, val type: String,
val width: Int, val width: Int,
val height: Int val height: Int
) : Parcelable { ) {
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode()

View File

@ -31,6 +31,7 @@ data class Notification(
REBLOG("reblog"), REBLOG("reblog"),
FAVOURITE("favourite"), FAVOURITE("favourite"),
FOLLOW("follow"), FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"),
POLL("poll"); POLL("poll");
companion object { companion object {
@ -43,7 +44,7 @@ data class Notification(
} }
return UNKNOWN return UNKNOWN
} }
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL) val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL)
} }
override fun toString(): String { override fun toString(): String {

View File

@ -9,6 +9,7 @@ data class Poll(
val expired: Boolean, val expired: Boolean,
val multiple: Boolean, val multiple: Boolean,
@SerializedName("votes_count") val votesCount: Int, @SerializedName("votes_count") val votesCount: Int,
@SerializedName("voters_count") val votersCount: Int?, // nullable for compatibility with Pleroma
val options: List<PollOption>, val options: List<PollOption>,
val voted: Boolean val voted: Boolean
) { ) {
@ -22,7 +23,12 @@ data class Poll(
} }
} }
return copy(options = newOptions, votesCount = votesCount + choices.size, voted = true) return copy(
options = newOptions,
votesCount = votesCount + choices.size,
votersCount = votersCount?.plus(1),
voted = true
)
} }
fun toNewPoll(creationDate: Date) = NewPoll( fun toNewPoll(creationDate: Date) = NewPoll(

View File

@ -43,6 +43,7 @@ data class Status(
val mentions: Array<Mention>, val mentions: Array<Mention>,
val application: Application?, val application: Application?,
var pinned: Boolean?, var pinned: Boolean?,
var muted: Boolean?,
val poll: Poll?, val poll: Poll?,
val card: Card?, val card: Card?,
val quote: Status? val quote: Status?

View File

@ -199,10 +199,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
val itemCount = layoutManager.itemCount val itemCount = layoutManager.itemCount
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition() val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) { if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
statuses.lastOrNull()?.let { last -> statuses.lastOrNull()?.let { (id) ->
Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)") Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)")
fetchingStatus = FetchingStatus.FETCHING_BOTTOM fetchingStatus = FetchingStatus.FETCHING_BOTTOM
currentCall = api.accountStatuses(accountId, last.id, null, null, null, true, null) currentCall = api.accountStatuses(accountId, id, null, null, null, true, null)
currentCall?.enqueue(bottomCallback) currentCall?.enqueue(bottomCallback)
} }
} }
@ -254,7 +254,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
if (view != null && activity != null) { if (view != null && activity != null) {
val url = items[currentIndex].attachment.url val url = items[currentIndex].attachment.url
ViewCompat.setTransitionName(view, url) ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, url) val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
} else { } else {
startActivity(intent) startActivity(intent)

View File

@ -65,10 +65,13 @@ import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.di.Injectable;
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.Relationship;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
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.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -97,6 +100,7 @@ import javax.inject.Inject;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit; import kotlin.Unit;
import kotlin.collections.CollectionsKt; import kotlin.collections.CollectionsKt;
@ -114,6 +118,7 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener, StatusActionListener,
NotificationsAdapter.NotificationActionListener, NotificationsAdapter.NotificationActionListener,
AccountActionListener,
Injectable, ReselectableFragment { Injectable, ReselectableFragment {
private static final String TAG = "NotificationF"; // logging tag private static final String TAG = "NotificationF"; // logging tag
@ -244,11 +249,13 @@ public class NotificationsFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true)
); );
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, statusDisplayOptions, this, this); dataSource, statusDisplayOptions, this, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
@ -767,6 +774,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_boost_name); return getString(R.string.notification_boost_name);
case FOLLOW: case FOLLOW:
return getString(R.string.notification_follow_name); return getString(R.string.notification_follow_name);
case FOLLOW_REQUEST:
return getString(R.string.notification_follow_request_name);
case POLL: case POLL:
return getString(R.string.notification_poll_name); return getString(R.string.notification_poll_name);
default: default:
@ -819,6 +828,29 @@ public class NotificationsFragment extends SFragment implements
super.viewAccount(id); super.viewAccount(id);
} }
@Override
public void onMute(boolean mute, String id, int position) {
// No muting from notifications yet
}
@Override
public void onBlock(boolean block, String id, int position) {
// No blocking from notifications yet
}
@Override
public void onRespondToFollowRequest(boolean accept, String id, int position) {
Single<Relationship> request = accept ?
mastodonApi.authorizeFollowRequestObservable(id) :
mastodonApi.rejectFollowRequestObservable(id);
request.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
(relationship) -> fullyRefreshWithProgressBar(true),
(error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))
);
}
@Override @Override
public void onViewStatusForNotificationId(String notificationId) { public void onViewStatusForNotificationId(String notificationId) {
for (Either<Placeholder, Notification> either : notifications) { for (Either<Placeholder, Notification> either : notifications) {

View File

@ -39,6 +39,7 @@ import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity; import com.keylesspalace.tusky.BottomSheetActivity;
@ -215,11 +216,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
PopupMenu popup = new PopupMenu(getContext(), view); PopupMenu popup = new PopupMenu(getContext(), view);
// Give a different menu depending on whether this is the user's own toot or not. // Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { boolean statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId.equals(accountId);
popup.inflate(R.menu.status_more); if (statusIsByCurrentUser) {
Menu menu = popup.getMenu();
menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty());
} else {
popup.inflate(R.menu.status_more_for_user); popup.inflate(R.menu.status_more_for_user);
Menu menu = popup.getMenu(); Menu menu = popup.getMenu();
switch (status.getVisibility()) { switch (status.getVisibility()) {
@ -238,6 +236,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
break; break;
} }
} }
} else {
popup.inflate(R.menu.status_more);
Menu menu = popup.getMenu();
menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty());
} }
Menu menu = popup.getMenu(); Menu menu = popup.getMenu();
@ -261,6 +263,15 @@ public abstract class SFragment extends BaseFragment implements Injectable {
} }
openAsItem.setTitle(openAsTitle); openAsItem.setTitle(openAsTitle);
MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation);
boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions());
muteConversationItem.setVisible(mutable);
if (mutable) {
muteConversationItem.setTitle((status.getMuted() == null || !status.getMuted()) ?
R.string.action_mute_conversation :
R.string.action_unmute_conversation);
}
popup.setOnMenuItemClickListener(item -> { popup.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.status_share_content: { case R.id.status_share_content: {
@ -304,11 +315,11 @@ public abstract class SFragment extends BaseFragment implements Injectable {
return true; return true;
} }
case R.id.status_mute: { case R.id.status_mute: {
timelineCases.mute(accountId); onMute(accountId, accountUsername);
return true; return true;
} }
case R.id.status_block: { case R.id.status_block: {
timelineCases.block(accountId); onBlock(accountId, accountUsername);
return true; return true;
} }
case R.id.status_report: { case R.id.status_report: {
@ -335,12 +346,52 @@ public abstract class SFragment extends BaseFragment implements Injectable {
timelineCases.pin(status, !status.isPinned()); timelineCases.pin(status, !status.isPinned());
return true; return true;
} }
case R.id.status_mute_conversation: {
timelineCases.muteConversation(status, status.getMuted() == null || !status.getMuted())
.onErrorReturnItem(status)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe();
return true;
}
} }
return false; return false;
}); });
popup.show(); popup.show();
} }
private void onMute(String accountId, String accountUsername) {
new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_mute_warning, accountUsername))
.setPositiveButton(android.R.string.ok, (__, ___) -> timelineCases.mute(accountId))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void onBlock(String accountId, String accountUsername) {
new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok, (__, ___) -> timelineCases.block(accountId))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private static boolean accountIsInMentions(AccountEntity account, Status.Mention[] mentions) {
if (account == null) {
return false;
}
for (Status.Mention mention : mentions) {
if (account.getUsername().equals(mention.getUsername())) {
Uri uri = Uri.parse(mention.getUrl());
if (uri != null && account.getDomain().equals(uri.getHost())) {
return true;
}
}
}
return false;
}
protected void viewMedia(int urlIndex, Status status, @Nullable View view) { protected void viewMedia(int urlIndex, Status status, @Nullable View view) {
final Status actionable = status.getActionableStatus(); final Status actionable = status.getActionableStatus();
final Attachment active = actionable.getAttachments().get(urlIndex); final Attachment active = actionable.getAttachments().get(urlIndex);

View File

@ -56,6 +56,7 @@ import com.keylesspalace.tusky.appstore.BookmarkEvent;
import com.keylesspalace.tusky.appstore.DomainMuteEvent; import com.keylesspalace.tusky.appstore.DomainMuteEvent;
import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.FavoriteEvent;
import com.keylesspalace.tusky.appstore.MuteConversationEvent;
import com.keylesspalace.tusky.appstore.MuteEvent; import com.keylesspalace.tusky.appstore.MuteEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
import com.keylesspalace.tusky.appstore.QuickReplyEvent; import com.keylesspalace.tusky.appstore.QuickReplyEvent;
@ -78,6 +79,7 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.repository.Placeholder; import com.keylesspalace.tusky.repository.Placeholder;
import com.keylesspalace.tusky.repository.TimelineRepository; import com.keylesspalace.tusky.repository.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode; import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -229,7 +231,7 @@ public class TimelineFragment extends SFragment implements
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bundle arguments = Objects.requireNonNull(getArguments()); Bundle arguments = requireArguments();
kind = Kind.valueOf(arguments.getString(KIND_ARG)); kind = Kind.valueOf(arguments.getString(KIND_ARG));
if (kind == Kind.TAG if (kind == Kind.TAG
|| kind == Kind.USER || kind == Kind.USER
@ -245,7 +247,11 @@ public class TimelineFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED :
CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true)
); );
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
@ -568,6 +574,9 @@ public class TimelineFragment extends SFragment implements
} else if (event instanceof BookmarkEvent) { } else if (event instanceof BookmarkEvent) {
BookmarkEvent bookmarkEvent = (BookmarkEvent) event; BookmarkEvent bookmarkEvent = (BookmarkEvent) event;
handleBookmarkEvent(bookmarkEvent); handleBookmarkEvent(bookmarkEvent);
} else if (event instanceof MuteConversationEvent) {
MuteConversationEvent muteEvent = (MuteConversationEvent) event;
handleMuteConversationEvent(muteEvent);
} else if (event instanceof UnfollowEvent) { } else if (event instanceof UnfollowEvent) {
if (kind == Kind.HOME) { if (kind == Kind.HOME) {
String id = ((UnfollowEvent) event).getAccountId(); String id = ((UnfollowEvent) event).getAccountId();
@ -1428,6 +1437,10 @@ public class TimelineFragment extends SFragment implements
setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark()); setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark());
} }
private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) {
fullyRefresh();
}
private void handleStatusComposeEvent(@NonNull Status status) { private void handleStatusComposeEvent(@NonNull Status status) {
switch (kind) { switch (kind) {
case HOME: case HOME:

View File

@ -89,7 +89,7 @@ class ViewImageFragment : ViewMediaFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = activity!!.toolbar toolbar = requireActivity().toolbar
this.transition = BehaviorSubject.create() this.transition = BehaviorSubject.create()
return inflater.inflate(R.layout.fragment_view_image, container, false) return inflater.inflate(R.layout.fragment_view_image, container, false)
} }
@ -97,7 +97,7 @@ class ViewImageFragment : ViewMediaFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val arguments = this.arguments!! val arguments = this.requireArguments()
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT) val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION) this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
val url: String? val url: String?

View File

@ -58,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -131,7 +132,11 @@ public final class ViewThreadFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false), preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true) preferences.getBoolean("useBlurhash", true),
preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED :
CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true)
); );
adapter = new ThreadAdapter(statusDisplayOptions, this); adapter = new ThreadAdapter(statusDisplayOptions, this);
} }

View File

@ -136,12 +136,12 @@ class ViewVideoFragment : ViewMediaFragment() {
progressBar.hide() progressBar.hide()
mp.isLooping = true mp.isLooping = true
if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) { if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
videoView.start() videoView.start()
} }
} }
if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) { if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
mediaActivity.onBringUp() mediaActivity.onBringUp()
} }
} }
@ -151,7 +151,7 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = activity!!.toolbar toolbar = requireActivity().toolbar
mediaActivity = activity as ViewMediaActivity mediaActivity = activity as ViewMediaActivity
return inflater.inflate(R.layout.fragment_view_video, container, false) return inflater.inflate(R.layout.fragment_view_video, container, false)
} }

View File

@ -41,42 +41,23 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
val activeAccount = accountManager.activeAccount val activeAccount = accountManager.activeAccount
if (activeAccount != null) { if (activeAccount != null) {
for (pair in mapOf(
val notificationPref = requirePreference("notificationsEnabled") as SwitchPreferenceCompat "notificationsEnabled" to activeAccount.notificationsEnabled,
notificationPref.isChecked = activeAccount.notificationsEnabled "notificationFilterMentions" to activeAccount.notificationsMentioned,
notificationPref.onPreferenceChangeListener = this "notificationFilterFollows" to activeAccount.notificationsFollowed,
"notificationFilterFollowRequests" to activeAccount.notificationsFollowRequested,
val mentionedPref = requirePreference("notificationFilterMentions") as SwitchPreferenceCompat "notificationFilterReblogs" to activeAccount.notificationsReblogged,
mentionedPref.isChecked = activeAccount.notificationsMentioned "notificationFilterFavourites" to activeAccount.notificationsFavorited,
mentionedPref.onPreferenceChangeListener = this "notificationFilterPolls" to activeAccount.notificationsPolls,
"notificationAlertSound" to activeAccount.notificationSound,
val followedPref = requirePreference("notificationFilterFollows") as SwitchPreferenceCompat "notificationAlertVibrate" to activeAccount.notificationVibration,
followedPref.isChecked = activeAccount.notificationsFollowed "notificationAlertLight" to activeAccount.notificationLight
followedPref.onPreferenceChangeListener = this )) {
(requirePreference(pair.key) as SwitchPreferenceCompat).apply {
val boostedPref = requirePreference("notificationFilterReblogs") as SwitchPreferenceCompat isChecked = pair.value
boostedPref.isChecked = activeAccount.notificationsReblogged onPreferenceChangeListener = this@NotificationPreferencesFragment
boostedPref.onPreferenceChangeListener = this }
}
val favoritedPref = requirePreference("notificationFilterFavourites") as SwitchPreferenceCompat
favoritedPref.isChecked = activeAccount.notificationsFavorited
favoritedPref.onPreferenceChangeListener = this
val pollsPref = requirePreference("notificationFilterPolls") as SwitchPreferenceCompat
pollsPref.isChecked = activeAccount.notificationsPolls
pollsPref.onPreferenceChangeListener = this
val soundPref = requirePreference("notificationAlertSound") as SwitchPreferenceCompat
soundPref.isChecked = activeAccount.notificationSound
soundPref.onPreferenceChangeListener = this
val vibrationPref = requirePreference("notificationAlertVibrate") as SwitchPreferenceCompat
vibrationPref.isChecked = activeAccount.notificationVibration
vibrationPref.onPreferenceChangeListener = this
val lightPref = requirePreference("notificationAlertLight") as SwitchPreferenceCompat
lightPref.isChecked = activeAccount.notificationLight
lightPref.onPreferenceChangeListener = this
} }
} }
@ -96,6 +77,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
} }
"notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean "notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean
"notificationFilterFollows" -> activeAccount.notificationsFollowed = newValue as Boolean "notificationFilterFollows" -> activeAccount.notificationsFollowed = newValue as Boolean
"notificationFilterFollowRequests" -> activeAccount.notificationsFollowRequested = newValue as Boolean
"notificationFilterReblogs" -> activeAccount.notificationsReblogged = newValue as Boolean "notificationFilterReblogs" -> activeAccount.notificationsReblogged = newValue as Boolean
"notificationFilterFavourites" -> activeAccount.notificationsFavorited = newValue as Boolean "notificationFilterFavourites" -> activeAccount.notificationsFavorited = newValue as Boolean
"notificationFilterPolls" -> activeAccount.notificationsPolls = newValue as Boolean "notificationFilterPolls" -> activeAccount.notificationsPolls = newValue as Boolean

View File

@ -18,6 +18,8 @@ package com.keylesspalace.tusky.json;
import android.text.Spanned; import android.text.Spanned;
import android.text.SpannedString; import android.text.SpannedString;
import androidx.core.text.HtmlCompat;
import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -25,7 +27,6 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer; import com.google.gson.JsonSerializer;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -35,7 +36,9 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
throws JsonParseException { throws JsonParseException {
String string = json.getAsString(); String string = json.getAsString();
if (string != null) { if (string != null) {
return HtmlUtils.fromHtml(string); /* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
* all status contents do, so it should be trimmed. */
return (Spanned)trimTrailingWhitespace(HtmlCompat.fromHtml(string, HtmlCompat.FROM_HTML_MODE_LEGACY));
} else { } else {
return new SpannedString(""); return new SpannedString("");
} }
@ -43,6 +46,14 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
@Override @Override
public JsonElement serialize(Spanned src, Type typeOfSrc, JsonSerializationContext context) { public JsonElement serialize(Spanned src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(HtmlUtils.toHtml(src)); return new JsonPrimitive(HtmlCompat.toHtml(src, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL));
}
private static CharSequence trimTrailingWhitespace(CharSequence s) {
int i = s.length();
do {
i--;
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
return s.subSequence(0, i + 1);
} }
} }

View File

@ -200,6 +200,16 @@ interface MastodonApi {
@Path("id") statusId: String @Path("id") statusId: String
): Single<Status> ): Single<Status>
@POST("api/v1/statuses/{id}/mute")
fun muteConversation(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/unmute")
fun unmuteConversation(
@Path("id") statusId: String
): Single<Status>
@GET("api/v1/scheduled_statuses") @GET("api/v1/scheduled_statuses")
fun scheduledStatuses( fun scheduledStatuses(
@Query("limit") limit: Int? = null, @Query("limit") limit: Int? = null,
@ -383,6 +393,16 @@ interface MastodonApi {
@Path("id") accountId: String @Path("id") accountId: String
): Call<Relationship> ): Call<Relationship>
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship>
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/apps") @POST("api/v1/apps")
fun authenticateApp( fun authenticateApp(

View File

@ -41,7 +41,7 @@ interface TimelineCases {
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean) fun pin(status: Status, pin: Boolean)
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll> fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
fun muteConversation(status: Status, mute: Boolean): Single<Status>
} }
class TimelineCasesImpl( class TimelineCasesImpl(
@ -94,6 +94,19 @@ class TimelineCasesImpl(
} }
} }
override fun muteConversation(status: Status, mute: Boolean): Single<Status> {
val id = status.actionableId
val call = if (mute) {
mastodonApi.muteConversation(id)
} else {
mastodonApi.unmuteConversation(id)
}
return call.doAfterSuccess {
eventHub.dispatch(MuteConversationEvent(status.id, mute))
}
}
override fun mute(id: String) { override fun mute(id: String) {
val call = mastodonApi.muteAccount(id) val call = mastodonApi.muteAccount(id)
call.enqueue(object : Callback<Relationship> { call.enqueue(object : Callback<Relationship> {

View File

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.repository package com.keylesspalace.tusky.repository
import android.text.SpannedString import android.text.SpannedString
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.db.*
@ -9,7 +11,6 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK
import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.inc import com.keylesspalace.tusky.util.inc
import io.reactivex.Single import io.reactivex.Single
@ -41,8 +42,7 @@ class TimelineRepositoryImpl(
private val timelineDao: TimelineDao, private val timelineDao: TimelineDao,
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val gson: Gson, private val gson: Gson
private val htmlConverter: HtmlConverter
) : TimelineRepository { ) : TimelineRepository {
init { init {
@ -67,7 +67,7 @@ class TimelineRepositoryImpl(
val accountId = acc.id val accountId = acc.id
timelineDao.insertInTransaction( timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson), status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson), status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson) status.reblog?.account?.toEntity(accountId, gson)
) )
@ -162,7 +162,7 @@ class TimelineRepositoryImpl(
for (status in statuses) { for (status in statuses) {
timelineDao.insertInTransaction( timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson), status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson), status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson) status.reblog?.account?.toEntity(accountId, gson)
) )
@ -226,7 +226,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""), content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
@ -241,6 +241,7 @@ class TimelineRepositoryImpl(
mentions = mentions, mentions = mentions,
application = application, application = application,
pinned = false, pinned = false,
muted = status.muted,
poll = poll, poll = poll,
card = null, card = null,
quote = null quote = null
@ -269,6 +270,7 @@ class TimelineRepositoryImpl(
mentions = arrayOf(), mentions = arrayOf(),
application = null, application = null,
pinned = false, pinned = false,
muted = status.muted,
poll = null, poll = null,
card = null, card = null,
quote = null quote = null
@ -281,7 +283,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""), content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
@ -296,6 +298,7 @@ class TimelineRepositoryImpl(
mentions = mentions, mentions = mentions,
application = application, application = application,
pinned = false, pinned = false,
muted = status.muted,
poll = poll, poll = poll,
card = null, card = null,
quote = null quote = null
@ -368,12 +371,12 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
application = null, application = null,
reblogServerId = null, reblogServerId = null,
reblogAccountId = null, reblogAccountId = null,
poll = null poll = null,
muted = false
) )
} }
fun Status.toEntity(timelineUserId: Long, fun Status.toEntity(timelineUserId: Long,
htmlConverter: HtmlConverter,
gson: Gson): TimelineStatusEntity { gson: Gson): TimelineStatusEntity {
val actionable = actionableStatus val actionable = actionableStatus
return TimelineStatusEntity( return TimelineStatusEntity(
@ -383,7 +386,7 @@ fun Status.toEntity(timelineUserId: Long,
authorServerId = actionable.account.id, authorServerId = actionable.account.id,
inReplyToId = actionable.inReplyToId, inReplyToId = actionable.inReplyToId,
inReplyToAccountId = actionable.inReplyToAccountId, inReplyToAccountId = actionable.inReplyToAccountId,
content = htmlConverter.toHtml(actionable.content), content = actionable.content.toHtml(),
createdAt = actionable.createdAt.time, createdAt = actionable.createdAt.time,
emojis = actionable.emojis.let(gson::toJson), emojis = actionable.emojis.let(gson::toJson),
reblogsCount = actionable.reblogsCount, reblogsCount = actionable.reblogsCount,
@ -396,10 +399,11 @@ fun Status.toEntity(timelineUserId: Long,
visibility = actionable.visibility, visibility = actionable.visibility,
attachments = actionable.attachments.let(gson::toJson), attachments = actionable.attachments.let(gson::toJson),
mentions = actionable.mentions.let(gson::toJson), mentions = actionable.mentions.let(gson::toJson),
application = actionable.let(gson::toJson), application = actionable.application.let(gson::toJson),
reblogServerId = reblog?.id, reblogServerId = reblog?.id,
reblogAccountId = reblog?.let { this.account.id }, reblogAccountId = reblog?.let { this.account.id },
poll = actionable.poll.let(gson::toJson) poll = actionable.poll.let(gson::toJson),
muted = actionable.muted
) )
} }

View File

@ -0,0 +1,7 @@
package com.keylesspalace.tusky.util
enum class CardViewMode {
NONE,
FULL_WIDTH,
INDENTED
}

View File

@ -1,22 +0,0 @@
package com.keylesspalace.tusky.util
import android.text.Spanned
/**
* Abstracting away Android-specific things.
*/
interface HtmlConverter {
fun fromHtml(html: String): Spanned
fun toHtml(text: Spanned): String
}
internal class HtmlConverterImpl : HtmlConverter {
override fun fromHtml(html: String): Spanned {
return HtmlUtils.fromHtml(html)
}
override fun toHtml(text: Spanned): String {
return HtmlUtils.toHtml(text)
}
}

View File

@ -1,52 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.os.Build;
import android.text.Html;
import android.text.Spanned;
public class HtmlUtils {
private static CharSequence trimTrailingWhitespace(CharSequence s) {
int i = s.length();
do {
i--;
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
return s.subSequence(0, i + 1);
}
public static Spanned fromHtml(String html) {
Spanned result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
} else {
result = Html.fromHtml(html);
}
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
* all status contents do, so it should be trimmed. */
return (Spanned) trimTrailingWhitespace(result);
}
public static String toHtml(Spanned text) {
String result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result = Html.toHtml(text, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
} else {
result = Html.toHtml(text);
}
return result;
}
}

View File

@ -82,6 +82,8 @@ class ListStatusAccessibilityDelegate(
} }
if (status.reblogsCount > 0) info.addAction(openRebloggedByAction) if (status.reblogsCount > 0) info.addAction(openRebloggedByAction)
if (status.favouritesCount > 0) info.addAction(openFavsAction) if (status.favouritesCount > 0) info.addAction(openFavsAction)
info.addAction(moreAction)
} }
} }
@ -150,6 +152,9 @@ class ListStatusAccessibilityDelegate(
interrupt() interrupt()
statusActionListener.onShowFavs(pos) statusActionListener.onShowFavs(pos)
} }
R.id.action_more -> {
statusActionListener.onMore(host, pos)
}
else -> return super.performAccessibilityAction(host, action, args) else -> return super.performAccessibilityAction(host, action, args)
} }
return true return true
@ -311,5 +316,10 @@ class ListStatusAccessibilityDelegate(
R.id.action_open_faved_by, R.id.action_open_faved_by,
context.getString(R.string.action_open_faved_by)) context.getString(R.string.action_open_faved_by))
private val moreAction = AccessibilityActionCompat(
R.id.action_more,
context.getString(R.string.action_more)
)
private data class LinkSpanInfo(val text: String, val link: String) private data class LinkSpanInfo(val text: String, val link: String)
} }

View File

@ -113,6 +113,7 @@ public class NotificationHelper {
**/ **/
public static final String CHANNEL_MENTION = "CHANNEL_MENTION"; public static final String CHANNEL_MENTION = "CHANNEL_MENTION";
public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW"; public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW";
public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST";
public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; public static final String CHANNEL_BOOST = "CHANNEL_BOOST";
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";
@ -348,6 +349,7 @@ public class NotificationHelper {
String[] channelIds = new String[]{ String[] channelIds = new String[]{
CHANNEL_MENTION + account.getIdentifier(), CHANNEL_MENTION + account.getIdentifier(),
CHANNEL_FOLLOW + account.getIdentifier(), CHANNEL_FOLLOW + account.getIdentifier(),
CHANNEL_FOLLOW_REQUEST + account.getIdentifier(),
CHANNEL_BOOST + account.getIdentifier(), CHANNEL_BOOST + account.getIdentifier(),
CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(),
@ -355,6 +357,7 @@ public class NotificationHelper {
int[] channelNames = { int[] channelNames = {
R.string.notification_mention_name, R.string.notification_mention_name,
R.string.notification_follow_name, R.string.notification_follow_name,
R.string.notification_follow_request_name,
R.string.notification_boost_name, R.string.notification_boost_name,
R.string.notification_favourite_name, R.string.notification_favourite_name,
R.string.notification_poll_name R.string.notification_poll_name
@ -362,12 +365,13 @@ public class NotificationHelper {
int[] channelDescriptions = { int[] channelDescriptions = {
R.string.notification_mention_descriptions, R.string.notification_mention_descriptions,
R.string.notification_follow_description, R.string.notification_follow_description,
R.string.notification_follow_request_description,
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
}; };
List<NotificationChannel> channels = new ArrayList<>(5); List<NotificationChannel> channels = new ArrayList<>(6);
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
@ -508,6 +512,8 @@ public class NotificationHelper {
return account.getNotificationsMentioned(); return account.getNotificationsMentioned();
case FOLLOW: case FOLLOW:
return account.getNotificationsFollowed(); return account.getNotificationsFollowed();
case FOLLOW_REQUEST:
return account.getNotificationsFollowRequested();
case REBLOG: case REBLOG:
return account.getNotificationsReblogged(); return account.getNotificationsReblogged();
case FAVOURITE: case FAVOURITE:
@ -525,6 +531,8 @@ public class NotificationHelper {
return CHANNEL_MENTION + account.getIdentifier(); return CHANNEL_MENTION + account.getIdentifier();
case FOLLOW: case FOLLOW:
return CHANNEL_FOLLOW + account.getIdentifier(); return CHANNEL_FOLLOW + account.getIdentifier();
case FOLLOW_REQUEST:
return CHANNEL_FOLLOW_REQUEST + account.getIdentifier();
case REBLOG: case REBLOG:
return CHANNEL_BOOST + account.getIdentifier(); return CHANNEL_BOOST + account.getIdentifier();
case FAVOURITE: case FAVOURITE:
@ -594,6 +602,9 @@ public class NotificationHelper {
case FOLLOW: case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), return String.format(context.getString(R.string.notification_follow_format),
accountName); accountName);
case FOLLOW_REQUEST:
return String.format(context.getString(R.string.notification_follow_request_format),
accountName);
case FAVOURITE: case FAVOURITE:
return String.format(context.getString(R.string.notification_favourite_format), return String.format(context.getString(R.string.notification_favourite_format),
accountName); accountName);
@ -613,6 +624,7 @@ public class NotificationHelper {
private static String bodyForType(Notification notification, Context context) { private static String bodyForType(Notification notification, Context context) {
switch (notification.getType()) { switch (notification.getType()) {
case FOLLOW: case FOLLOW:
case FOLLOW_REQUEST:
return "@" + notification.getAccount().getUsername(); return "@" + notification.getAccount().getUsername();
case MENTION: case MENTION:
case FAVOURITE: case FAVOURITE:
@ -631,7 +643,7 @@ 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.getVotesCount()), PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
context)); context));
builder.append('\n'); builder.append('\n');
} }

View File

@ -10,5 +10,9 @@ data class StatusDisplayOptions(
@get:JvmName("showBotOverlay") @get:JvmName("showBotOverlay")
val showBotOverlay: Boolean, val showBotOverlay: Boolean,
@get:JvmName("useBlurhash") @get:JvmName("useBlurhash")
val useBlurhash: Boolean val useBlurhash: Boolean,
@get:JvmName("cardViewMode")
val cardViewMode: CardViewMode,
@get:JvmName("confirmReblogs")
val confirmReblogs: Boolean
) )

View File

@ -24,7 +24,6 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
@ -172,15 +171,12 @@ class StatusViewHelper(private val itemView: View) {
sensitiveMediaWarning.visibility = View.GONE sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE sensitiveMediaShow.visibility = View.GONE
} else { } else {
sensitiveMediaWarning.text = if (sensitive) {
val hiddenContentText: String = if (sensitive) {
context.getString(R.string.status_sensitive_media_title) context.getString(R.string.status_sensitive_media_title)
} else { } else {
context.getString(R.string.status_media_hidden_title) context.getString(R.string.status_media_hidden_title)
} }
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
sensitiveMediaShow.setOnClickListener { v -> sensitiveMediaShow.setOnClickListener { v ->
@ -275,17 +271,22 @@ class StatusViewHelper(private val itemView: View) {
private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence { private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
val context = pollDescription.context val context = pollDescription.context
val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
val votesText = context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes) val votesText = if(poll.votersCount == null) {
val pollDurationInfo: CharSequence val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
if (poll.expired) { context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes)
pollDurationInfo = context.getString(R.string.poll_info_closed) } else {
val votes = NumberFormat.getNumberInstance().format(poll.votersCount.toLong())
context.resources.getQuantityString(R.plurals.poll_info_people, poll.votersCount, votes)
}
val pollDurationInfo = if (poll.expired) {
context.getString(R.string.poll_info_closed)
} else { } else {
if (useAbsoluteTime) { if (useAbsoluteTime) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt)) context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
} else { } else {
val pollDuration = TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) val pollDuration = TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration) context.getString(R.string.poll_info_time_relative, pollDuration)
} }
} }
@ -298,7 +299,7 @@ class StatusViewHelper(private val itemView: View) {
for (i in 0 until Status.MAX_POLL_OPTIONS) { for (i in 0 until Status.MAX_POLL_OPTIONS) {
if (i < options.size) { if (i < options.size) {
val percent = calculatePercent(options[i].votesCount, poll.votesCount) val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount)
val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context) val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context)
pollResults[i].text = CustomEmojiHelper.emojifyText(pollOptionText, emojis, pollResults[i]) pollResults[i].text = CustomEmojiHelper.emojifyText(pollOptionText, emojis, pollResults[i])

View File

@ -18,10 +18,10 @@ package com.keylesspalace.tusky.viewdata
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.util.HtmlUtils
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -31,6 +31,7 @@ data class PollViewData(
val expired: Boolean, val expired: Boolean,
val multiple: Boolean, val multiple: Boolean,
val votesCount: Int, val votesCount: Int,
val votersCount: Int?,
val options: List<PollOptionViewData>, val options: List<PollOptionViewData>,
var voted: Boolean var voted: Boolean
) )
@ -41,16 +42,17 @@ data class PollOptionViewData(
var selected: Boolean var selected: Boolean
) )
fun calculatePercent(fraction: Int, total: Int): Int { fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
return if (fraction == 0) { return if (fraction == 0) {
0 0
} else { } else {
val total = totalVoters ?: totalVotes
(fraction / total.toDouble() * 100).roundToInt() (fraction / total.toDouble() * 100).roundToInt()
} }
} }
fun buildDescription(title: String, percent: Int, context: Context): Spanned { fun buildDescription(title: String, percent: Int, context: Context): Spanned {
return SpannableStringBuilder(HtmlUtils.fromHtml(context.getString(R.string.poll_percent_format, percent))) return SpannableStringBuilder(context.getString(R.string.poll_percent_format, percent).parseAsHtml())
.append(" ") .append(" ")
.append(title) .append(title)
} }
@ -58,20 +60,21 @@ fun buildDescription(title: String, percent: Int, context: Context): Spanned {
fun Poll?.toViewData(): PollViewData? { fun Poll?.toViewData(): PollViewData? {
if (this == null) return null if (this == null) return null
return PollViewData( return PollViewData(
id, id = id,
expiresAt, expiresAt = expiresAt,
expired, expired = expired,
multiple, multiple = multiple,
votesCount, votesCount = votesCount,
options.map { it.toViewData() }, votersCount = votersCount,
voted options = options.map { it.toViewData() },
voted = voted
) )
} }
fun PollOption.toViewData(): PollOptionViewData { fun PollOption.toViewData(): PollOptionViewData {
return PollOptionViewData( return PollOptionViewData(
title, title = title,
votesCount, votesCount = votesCount,
false selected = false
) )
} }

View File

@ -58,6 +58,7 @@ public abstract class StatusViewData {
final boolean reblogged; final boolean reblogged;
final boolean favourited; final boolean favourited;
final boolean bookmarked; final boolean bookmarked;
private final boolean muted;
@Nullable @Nullable
private final String spoilerText; private final String spoilerText;
private final Status.Visibility visibility; private final Status.Visibility visibility;
@ -96,7 +97,7 @@ public abstract class StatusViewData {
private final Status quote; private final Status quote;
private final boolean isNotestock; private final boolean isNotestock;
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, boolean muted,
@Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments, @Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingContent, String userFullName, String nickname, String avatar, boolean isShowingContent, String userFullName, String nickname, String avatar,
@ -119,6 +120,7 @@ public abstract class StatusViewData {
this.reblogged = reblogged; this.reblogged = reblogged;
this.favourited = favourited; this.favourited = favourited;
this.bookmarked = bookmarked; this.bookmarked = bookmarked;
this.muted = muted;
this.visibility = visibility; this.visibility = visibility;
this.attachments = attachments; this.attachments = attachments;
this.rebloggedByUsername = rebloggedByUsername; this.rebloggedByUsername = rebloggedByUsername;
@ -167,6 +169,10 @@ public abstract class StatusViewData {
return bookmarked; return bookmarked;
} }
public boolean isMuted() {
return muted;
}
@Nullable @Nullable
public String getSpoilerText() { public String getSpoilerText() {
return spoilerText; return spoilerText;
@ -419,6 +425,7 @@ public abstract class StatusViewData {
private boolean reblogged; private boolean reblogged;
private boolean favourited; private boolean favourited;
private boolean bookmarked; private boolean bookmarked;
private boolean muted;
private String spoilerText; private String spoilerText;
private Status.Visibility visibility; private Status.Visibility visibility;
private List<Attachment> attachments; private List<Attachment> attachments;
@ -457,6 +464,7 @@ public abstract class StatusViewData {
reblogged = viewData.reblogged; reblogged = viewData.reblogged;
favourited = viewData.favourited; favourited = viewData.favourited;
bookmarked = viewData.bookmarked; bookmarked = viewData.bookmarked;
muted = viewData.muted;
spoilerText = viewData.spoilerText; spoilerText = viewData.spoilerText;
visibility = viewData.visibility; visibility = viewData.visibility;
attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments);
@ -512,6 +520,11 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setMuted(boolean muted) {
this.muted = muted;
return this;
}
public Builder setSpoilerText(String spoilerText) { public Builder setSpoilerText(String spoilerText) {
this.spoilerText = spoilerText; this.spoilerText = spoilerText;
return this; return this;
@ -675,7 +688,7 @@ public abstract class StatusViewData {
if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
if (this.createdAt == null) createdAt = new Date(); if (this.createdAt == null) createdAt = new Date();
return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, muted, spoilerText,
visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,

View File

@ -255,10 +255,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:elevation="12dp" android:elevation="12dp"
android:paddingStart="16dp" android:paddingStart="24dp"
android:paddingTop="8dp" android:paddingTop="12dp"
android:paddingEnd="16dp" android:paddingEnd="24dp"
android:paddingBottom="52dp" android:paddingBottom="60dp"
app:behavior_hideable="true" app:behavior_hideable="true"
app:behavior_peekHeight="0dp" app:behavior_peekHeight="0dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="10dp">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/notificationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_person_add_24dp"
android:drawablePadding="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Someone requested to follow you" />
<ImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginTop="10dp"
android:contentDescription="@string/action_view_profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/displayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
tools:text="Display name" />
<TextView
android:id="@+id/usernameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/displayNameTextView"
tools:text="\@username" />
<ImageButton
android:id="@+id/acceptButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginTop="14dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_accept"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
app:srcCompat="@drawable/ic_check_24dp" />
<ImageButton
android:id="@+id/rejectButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginTop="14dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_reject"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
app:srcCompat="@drawable/ic_reject_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -158,6 +158,73 @@
app:layout_constraintTop_toBottomOf="@id/status_content_warning_button" app:layout_constraintTop_toBottomOf="@id/status_content_warning_button"
tools:text="This is a status" /> tools:text="This is a status" />
<LinearLayout
android:id="@+id/status_card_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/card_frame"
android:clipChildren="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="80dp"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@+id/status_content"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="gone">
<ImageView
android:id="@+id/card_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="1dp"
android:background="?attr/colorBackgroundAccent"
android:importantForAccessibility="no"
android:scaleType="center" />
<LinearLayout
android:id="@+id/card_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="6dp"
android:paddingTop="6dp"
android:paddingRight="6dp"
android:paddingBottom="6dp">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:lines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/card_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.1"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<TextView
android:id="@+id/card_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
</LinearLayout>
<Button <Button
android:id="@+id/button_toggle_content" android:id="@+id/button_toggle_content"
style="@style/TuskyButton.Outlined" style="@style/TuskyButton.Outlined"
@ -176,7 +243,7 @@
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/status_content" app:layout_constraintTop_toBottomOf="@id/status_card_view"
tools:text="@string/status_content_show_less" tools:text="@string/status_content_show_less"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@ -143,7 +143,7 @@
app:layout_constraintTop_toBottomOf="@id/status_content" /> app:layout_constraintTop_toBottomOf="@id/status_content" />
<LinearLayout <LinearLayout
android:id="@+id/card_view" android:id="@+id/status_card_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@ -216,7 +216,7 @@
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:background="@drawable/media_preview_outline" android:background="@drawable/media_preview_outline"
android:importantForAccessibility="noHideDescendants" android:importantForAccessibility="noHideDescendants"
app:layout_constraintTop_toBottomOf="@id/card_view"> app:layout_constraintTop_toBottomOf="@id/status_card_view">
<com.keylesspalace.tusky.view.MediaPreviewImageView <com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0" android:id="@+id/status_media_preview_0"

View File

@ -1,55 +1,50 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.LinearLayout"> tools:layout_height="wrap_content"
tools:layout_width="match_parent"
tools:parentTag="RadioGroup">
<RadioGroup <RadioButton
android:id="@+id/visibilityRadioGroup" android:id="@+id/publicRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_weight="1"
android:button="@drawable/ic_public_24dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_public"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
<RadioButton
android:id="@+id/unlistedRadioButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/compose_options_margin"
android:layout_marginLeft="@dimen/compose_options_margin"
android:layout_marginRight="@dimen/compose_options_margin"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:orientation="vertical"> android:layout_marginBottom="4dp"
android:layout_weight="1"
android:button="@drawable/ic_lock_open_24dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_unlisted"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
<RadioButton <RadioButton
android:id="@+id/publicRadioButton" android:id="@+id/privateRadioButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginTop="4dp"
android:layout_weight="1" android:layout_marginBottom="4dp"
android:paddingEnd="0dp" android:layout_weight="1"
android:paddingStart="10dp" android:button="@drawable/ic_lock_outline_24dp"
android:text="@string/visibility_public" android:paddingStart="10dp"
android:textColor="?android:textColorTertiary" android:paddingEnd="0dp"
app:buttonTint="@color/compound_button_color" /> android:text="@string/visibility_private"
android:textColor="?android:textColorTertiary"
<RadioButton app:buttonTint="@color/compound_button_color" />
android:id="@+id/unlistedRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:paddingEnd="0dp"
android:paddingStart="10dp"
android:text="@string/visibility_unlisted"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
<RadioButton
android:id="@+id/privateRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:paddingEnd="0dp"
android:paddingStart="10dp"
android:text="@string/visibility_private"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
<RadioButton <RadioButton
android:id="@+id/unleakableRadioButton" android:id="@+id/unleakableRadioButton"
@ -58,8 +53,9 @@
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_weight="1" android:layout_weight="1"
android:paddingEnd="0dp" android:button="@drawable/ic_reblog_unleakable_24dp"
android:paddingStart="10dp" android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_unleakable" android:text="@string/visibility_unleakable"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" app:buttonTint="@color/compound_button_color"
@ -71,12 +67,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_weight="1" android:layout_weight="1"
android:paddingEnd="0dp" android:button="@drawable/ic_email_24dp"
android:paddingStart="10dp" android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_direct" android:text="@string/visibility_direct"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" /> app:buttonTint="@color/compound_button_color" />
</RadioGroup>
</merge> </merge>

View File

@ -21,6 +21,9 @@
<item <item
android:id="@+id/status_download_media" android:id="@+id/status_download_media"
android:title="@string/download_media" /> android:title="@string/download_media" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation" />
<item <item
android:id="@+id/status_mute" android:id="@+id/status_mute"
android:title="@string/action_mute" /> android:title="@string/action_mute" />

View File

@ -26,6 +26,9 @@
android:id="@+id/status_unreblog_private" android:id="@+id/status_unreblog_private"
android:title="@string/unreblog_private" android:title="@string/unreblog_private"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation" />
<item <item
android:id="@+id/status_delete" android:id="@+id/status_delete"
android:title="@string/action_delete" /> android:title="@string/action_delete" />

View File

@ -2,82 +2,82 @@
<resources> <resources>
<string name="error_generic">وقع هناك خطأ.</string> <string name="error_generic">وقع هناك خطأ.</string>
<string name="error_network">حدث خطأ في الشبكة! يرجى التحقق من اتصالك ثم أعد المحاولة!</string> <string name="error_network">حدث خطأ في الشبكة! يرجى التحقق من اتصالك ثم أعد المحاولة!</string>
<string name="error_empty">لا يجب أن يترك فارغا.</string> <string name="error_empty">لا يجب أن يترك هذا الحقل فارغا.</string>
<string name="error_invalid_domain">اسم النطاق غير صالح</string> <string name="error_invalid_domain">اسم النطاق الذي قمتَ بإدخاله غير صالح</string>
<string name="error_failed_app_registration">اخفقت المصادقة مع مثيل الخادم هذا.</string> <string name="error_failed_app_registration">اخفقت المصادقة مع مثيل الخادم هذا.</string>
<string name="error_no_web_browser_found">تعذر العثور على متصفح ويب قابل للإستعمال.</string> <string name="error_no_web_browser_found">تعذر العثور على متصفح ويب صالح للإستعمال.</string>
<string name="error_authorization_unknown">لقد وقع هناك خطأ مجهول في التصريح.</string> <string name="error_authorization_unknown">لقد وقع هناك خطأ مجهول في التصريح.</string>
<string name="error_authorization_denied">تم رفض التصريح.</string> <string name="error_authorization_denied">تم رفض التصريح.</string>
<string name="error_retrieving_oauth_token">فشل الحصول على رمز الدخول.</string> <string name="error_retrieving_oauth_token">فشل الحصول على رمز الولوج.</string>
<string name="error_compose_character_limit">المنشور طويل جدا !</string> <string name="error_compose_character_limit">إنّ المنشور طويل جدا!</string>
<string name="error_image_upload_size">يجب أن يكون حجم الملف أقل من 4 ميغابايت.</string> <string name="error_image_upload_size">يجب أن يكون حجم الملف أقل من 8 ميغابايت.</string>
<string name="error_video_upload_size">يجب أن يكون حجم ملفات الفيديو أقل من 40 ميغا بايت.</string> <string name="error_video_upload_size">يجب أن يكون حجم ملفات الفيديو أقل من 40 ميغا بايت.</string>
<string name="error_media_upload_type">لا يمكن رفع هذا النوع من الملفات.</string> <string name="error_media_upload_type">لا يمكن تحميل هذا النوع من الملفات.</string>
<string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string> <string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string>
<string name="error_media_upload_permission">التصريح لازم لقراءة الوسائط.</string> <string name="error_media_upload_permission">التصريح لازم لقراءة الوسائط.</string>
<string name="error_media_download_permission">التصريح لازم للإحتفاظ بالوسائط.</string> <string name="error_media_download_permission">التصريح لازم للإحتفاظ بالوسائط.</string>
<string name="error_media_upload_image_or_video">لا يمكنك إرفاق كلا من الصور و الفيديوهات في نفس المنشور.</string> <string name="error_media_upload_image_or_video">لا يمكنك إرفاق كلا من الصور والفيديوهات في نفس المنشور في آن واحد.</string>
<string name="error_media_upload_sending">اخفقت عملية الرفع.</string> <string name="error_media_upload_sending">اخفقت عملية الرفع.</string>
<string name="error_sender_account_gone">خطأ عند إرسال التبويق.</string> <string name="error_sender_account_gone">خطأ عند إرسال التبويق.</string>
<string name="title_home">الرئيسية</string> <string name="title_home">الرئيسي</string>
<string name="title_notifications">الاشعارات</string> <string name="title_notifications">الاشعارات</string>
<string name="title_public_local">المحلية</string> <string name="title_public_local">المحلي</string>
<string name="title_public_federated">الفدرالية</string> <string name="title_public_federated">الفدرالي</string>
<string name="title_direct_messages">الرسائل المباشرة</string> <string name="title_direct_messages">الرسائل المباشرة</string>
<string name="title_tab_preferences">الألسنة</string> <string name="title_tab_preferences">الألسنة</string>
<string name="title_view_thread">تبويق</string> <string name="title_view_thread">تبويق</string>
<string name="title_statuses">المشاركات</string> <string name="title_statuses">المنشورات</string>
<string name="title_statuses_with_replies">يحتوي على ردود</string> <string name="title_statuses_with_replies">التبويقات والردود</string>
<string name="title_statuses_pinned">مدبّس</string> <string name="title_statuses_pinned">المدبّسة</string>
<string name="title_follows">المتابَعون</string> <string name="title_follows">المتابَعون</string>
<string name="title_followers">المتابِعون</string> <string name="title_followers">المتابِعون</string>
<string name="title_favourites">المفضلة</string> <string name="title_favourites">المفضلة</string>
<string name="title_mutes">الحسابات المكتومة</string> <string name="title_mutes">الحسابات المكتومة</string>
<string name="title_blocks">الحسابات المحظورة</string> <string name="title_blocks">الحسابات المحظورة</string>
<string name="title_follow_requests">طلبات المتابعة</string> <string name="title_follow_requests">طلبات المتابعة</string>
<string name="title_edit_profile">عدل ملفك الشخصي</string> <string name="title_edit_profile">عدل ملفك التعريفي</string>
<string name="title_saved_toot">المسودات</string> <string name="title_saved_toot">المسودات</string>
<string name="title_licenses">الرّخص</string> <string name="title_licenses">الرّخص</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s رقّي</string> <string name="status_boosted_format">شارَكَه %s</string>
<string name="status_sensitive_media_title">محتوى حساس</string> <string name="status_sensitive_media_title">محتوى حساس</string>
<string name="status_media_hidden_title">وسائط مخفية</string> <string name="status_media_hidden_title">وسائط مخفية</string>
<string name="status_sensitive_media_directions">اضغط للعرض</string> <string name="status_sensitive_media_directions">اضغط للعرض</string>
<string name="status_content_warning_show_more">اعرض أكثر</string> <string name="status_content_warning_show_more">اعرض المزيد</string>
<string name="status_content_warning_show_less">اعرض أقل</string> <string name="status_content_warning_show_less">اعرض أقل</string>
<string name="status_content_show_more">توسيع</string> <string name="status_content_show_more">توسيع</string>
<string name="status_content_show_less">تصغير</string> <string name="status_content_show_less">تصغير</string>
<string name="message_empty">لا شيء هنا.</string> <string name="message_empty">لا شيء هنا.</string>
<string name="footer_empty">لا يوجد شيئ هنا. إسحب إلى أسفل للتحديث !</string> <string name="footer_empty">لا يوجد شيء هنا. إسحب إلى أسفل للإنعاش!</string>
<string name="notification_reblog_format">رقّى %s تبويقك</string> <string name="notification_reblog_format">شارَك %s تبويقك</string>
<string name="notification_favourite_format">أعجِب %s بتبويقك</string> <string name="notification_favourite_format">أعجِب %s بتبويقك</string>
<string name="notification_follow_format">%s يتبعك</string> <string name="notification_follow_format">%s يتبعك</string>
<string name="report_username_format">أبلغ عن @%s</string> <string name="report_username_format">أبلغ عن @%s</string>
<string name="report_comment_hint">تعليقات إضافية ؟</string> <string name="report_comment_hint">تعليقات إضافية؟</string>
<string name="action_quick_reply">إجابة سريعة</string> <string name="action_quick_reply">رد سريع</string>
<string name="action_reply">أجب</string> <string name="action_reply">رد</string>
<string name="action_reblog">رقّي</string> <string name="action_reblog">رقّي</string>
<string name="action_unreblog">إزالة الترقية</string> <string name="action_unreblog">إزالة الترقية</string>
<string name="action_favourite">تفضيل</string> <string name="action_favourite">تفضيل</string>
<string name="action_unfavourite">إزالة المفضلة</string> <string name="action_unfavourite">إزالة المفضلة</string>
<string name="action_more">المزيد</string> <string name="action_more">المزيد</string>
<string name="action_compose">حرر</string> <string name="action_compose">حرر</string>
<string name="action_login">التسجيل بواسطة ماستدون</string> <string name="action_login">الولوج إلى ماستدون</string>
<string name="action_logout">خروج</string> <string name="action_logout">خروج</string>
<string name="action_logout_confirm">متأكد مِن أنك تود الخروج من الحساب %1$s ؟</string> <string name="action_logout_confirm">متأكد مِن أنك تود الخروج من الحساب %1$s؟</string>
<string name="action_follow">إتبع</string> <string name="action_follow">إتبع</string>
<string name="action_unfollow">إلغاء التتبع</string> <string name="action_unfollow">إلغاء المتابعة</string>
<string name="action_block">قم بحظره</string> <string name="action_block">قم بحظره</string>
<string name="action_unblock">إلغاء الحظر</string> <string name="action_unblock">إلغاء الحظر</string>
<string name="action_hide_reblogs">إخفاء الترقيات</string> <string name="action_hide_reblogs">إخفاء الترقيات</string>
<string name="action_show_reblogs">إظهار الترقيات</string> <string name="action_show_reblogs">إظهار الترقيات</string>
<string name="action_report">أبلغ</string> <string name="action_report">أبلغ عنه</string>
<string name="action_delete">إحذف</string> <string name="action_delete">إحذف</string>
<string name="action_send">بَوّق</string> <string name="action_send">بَوّق</string>
<string name="action_send_public">بوّق!</string> <string name="action_send_public">بوّق!</string>
<string name="action_retry">إعادة المحاولة</string> <string name="action_retry">أعد المحاولة</string>
<string name="action_close">إغلاق</string> <string name="action_close">إغلاق</string>
<string name="action_view_profile">الملف الشخصي</string> <string name="action_view_profile">الملف التعريفي</string>
<string name="action_view_preferences">التفضيلات</string> <string name="action_view_preferences">التفضيلات</string>
<string name="action_view_account_preferences">تفضيلات الحساب</string> <string name="action_view_account_preferences">تفضيلات الحساب</string>
<string name="action_view_favourites">المفضلة</string> <string name="action_view_favourites">المفضلة</string>
@ -95,7 +95,7 @@
<string name="action_hide_media">إخفاء الوسائط</string> <string name="action_hide_media">إخفاء الوسائط</string>
<string name="action_open_drawer">إفتح الدرج</string> <string name="action_open_drawer">إفتح الدرج</string>
<string name="action_save">إحفظ</string> <string name="action_save">إحفظ</string>
<string name="action_edit_profile">تعديل الملف الشخصي</string> <string name="action_edit_profile">تعديل الملف التعريفي</string>
<string name="action_edit_own_profile">تعديل</string> <string name="action_edit_own_profile">تعديل</string>
<string name="action_undo">إلغاء</string> <string name="action_undo">إلغاء</string>
<string name="action_accept">موافقة</string> <string name="action_accept">موافقة</string>
@ -109,8 +109,8 @@
<string name="action_links">الروابط</string> <string name="action_links">الروابط</string>
<string name="action_mentions">الإشارات</string> <string name="action_mentions">الإشارات</string>
<string name="action_hashtags">الوسوم</string> <string name="action_hashtags">الوسوم</string>
<string name="action_open_reblogged_by">عرض الترقيات</string> <string name="action_open_reblogged_by">اعرض الترقيات</string>
<string name="action_open_faved_by">عرض المفضلات</string> <string name="action_open_faved_by">اعرض المفضلات</string>
<string name="title_hashtags_dialog">الوسوم</string> <string name="title_hashtags_dialog">الوسوم</string>
<string name="title_mentions_dialog">الإشارات</string> <string name="title_mentions_dialog">الإشارات</string>
<string name="title_links_dialog">الروابط</string> <string name="title_links_dialog">الروابط</string>
@ -120,23 +120,23 @@
<string name="action_share_as">شاركه كـ…</string> <string name="action_share_as">شاركه كـ…</string>
<string name="send_status_link_to">شارك رابط التبويق مع…</string> <string name="send_status_link_to">شارك رابط التبويق مع…</string>
<string name="send_status_content_to">شارك التبويق على…</string> <string name="send_status_content_to">شارك التبويق على…</string>
<string name="send_media_to">شارك رابط التبويق مع…</string> <string name="send_media_to">شارك الوسيط مع…</string>
<string name="confirmation_reported">تم الإرسال !</string> <string name="confirmation_reported">تم إرساله!</string>
<string name="confirmation_unblocked">تم فك الحجب عن الحساب</string> <string name="confirmation_unblocked">تم فك الحجب عن الحساب</string>
<string name="confirmation_unmuted">تم فك الكتم عن الحساب</string> <string name="confirmation_unmuted">لم يعد الحساب مكتومًا</string>
<string name="status_sent">تم إرساله !</string> <string name="status_sent">تم إرساله!</string>
<string name="status_sent_long">تم إرسال الرد بنجاح.</string> <string name="status_sent_long">تم إرسال الرد بنجاح.</string>
<string name="hint_domain">أي سيرفر ؟</string> <string name="hint_domain">أي مثيل خادم؟</string>
<string name="hint_compose">ما الجديد ؟</string> <string name="hint_compose">ما الجديد؟</string>
<string name="hint_content_warning">تحذير عن المحتوى</string> <string name="hint_content_warning">تحذير عن المحتوى</string>
<string name="hint_display_name">الإسم العلني</string> <string name="hint_display_name">الإسم العلني</string>
<string name="hint_note">السيرة</string> <string name="hint_note">السيرة</string>
<string name="hint_search">البحث عن…</string> <string name="hint_search">البحث عن…</string>
<string name="search_no_results">لم يتم العثور على نتائج</string> <string name="search_no_results">لم يتم العثور على أية نتائج</string>
<string name="label_quick_reply">إجابة </string> <string name="label_quick_reply">رد</string>
<string name="label_avatar">الصورة الرمزية</string> <string name="label_avatar">صورة الملف التعريفي</string>
<string name="label_header">رأس الصفحة</string> <string name="label_header">صورة رأس الصفحة</string>
<string name="link_whats_an_instance">ماذا نعني بمثيل الخادم ؟</string> <string name="link_whats_an_instance">ماذا نعني بمثيل الخادم؟</string>
<string name="login_connection">الإتصال جارٍ…</string> <string name="login_connection">الإتصال جارٍ…</string>
<string name="dialog_whats_an_instance">بإمكانك إدخال عنوان أي مثيل خادوم ماستدون هنا. على سبيل المثال mastodon.social أو icosahedron.website أو social.tchncs.de أوالإطلاع على <a href="https://instances.social">لاكتشاف المزيد !</a> <string name="dialog_whats_an_instance">بإمكانك إدخال عنوان أي مثيل خادوم ماستدون هنا. على سبيل المثال mastodon.social أو icosahedron.website أو social.tchncs.de أوالإطلاع على <a href="https://instances.social">لاكتشاف المزيد !</a>
\n \n
@ -148,13 +148,13 @@
<string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string> <string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string>
<string name="dialog_message_uploading_media">الإرسال جارٍ…</string> <string name="dialog_message_uploading_media">الإرسال جارٍ…</string>
<string name="dialog_download_image">تنزيل</string> <string name="dialog_download_image">تنزيل</string>
<string name="dialog_message_cancel_follow_request">هل تريد رفض طلب المتابعة ؟</string> <string name="dialog_message_cancel_follow_request">هل تريد رفض طلب المتابعة؟</string>
<string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب ؟</string> <string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب؟</string>
<string name="dialog_delete_toot_warning">هل تريد حذف هذا التبويق؟</string> <string name="dialog_delete_toot_warning">هل تريد حذف هذا التبويق؟</string>
<string name="visibility_public">عمومي : ينشر على الخيوط العمومية</string> <string name="visibility_public">للعامة: ينشر على الخيوط العمومية</string>
<string name="visibility_unlisted">غير مدرج : لا يُعرَض على الخيوط العمومية</string> <string name="visibility_unlisted">غير مدرج: لا يُعرَض على الخيوط العمومية</string>
<string name="visibility_private">لمتابعيك فقط : يُنشر إلى متابعيك فقط</string> <string name="visibility_private">لمتابعيك فقط: يُنشر إلى متابعيك فقط</string>
<string name="visibility_direct">مباشر : يُنشر إلى المستخدمين المشار إليهم فقط</string> <string name="visibility_direct">مباشر: يُنشر إلى المستخدمين المشار إليهم فقط</string>
<string name="pref_title_edit_notification_settings">تعديل الاشعارات</string> <string name="pref_title_edit_notification_settings">تعديل الاشعارات</string>
<string name="pref_title_notifications_enabled">الإخطارات</string> <string name="pref_title_notifications_enabled">الإخطارات</string>
<string name="pref_title_notification_alerts">التنبيهات</string> <string name="pref_title_notification_alerts">التنبيهات</string>
@ -163,12 +163,12 @@
<string name="pref_title_notification_alert_light">إعلام بالضوء</string> <string name="pref_title_notification_alert_light">إعلام بالضوء</string>
<string name="pref_title_notification_filters">أخطرني عندما</string> <string name="pref_title_notification_filters">أخطرني عندما</string>
<string name="pref_title_notification_filter_mentions">يشار إلي</string> <string name="pref_title_notification_filter_mentions">يشار إلي</string>
<string name="pref_title_notification_filter_follows">يتبعني أحد</string> <string name="pref_title_notification_filter_follows">يتبعني أحدهم</string>
<string name="pref_title_notification_filter_reblogs">تُرقّى منشوراتي</string> <string name="pref_title_notification_filter_reblogs">تُرقّى منشوراتي</string>
<string name="pref_title_notification_filter_favourites">أعجب أحد ما بمنشوراتي</string> <string name="pref_title_notification_filter_favourites">يُعجَب أحد ما بمنشوراتي</string>
<string name="pref_title_appearance_settings">المظهر</string> <string name="pref_title_appearance_settings">المظهر</string>
<string name="pref_title_app_theme">سمة التطبيق</string> <string name="pref_title_app_theme">حُلّة التطبيق</string>
<string name="pref_title_timelines">الخيوط</string> <string name="pref_title_timelines">الخيوط الزمنية</string>
<string name="pref_title_timeline_filters">عوامل التصفية</string> <string name="pref_title_timeline_filters">عوامل التصفية</string>
<string name="app_them_dark">داكنة</string> <string name="app_them_dark">داكنة</string>
<string name="app_theme_light">فاتحة</string> <string name="app_theme_light">فاتحة</string>
@ -339,7 +339,7 @@
<string name="action_open_reblogger">إظهار صاحب الترقية</string> <string name="action_open_reblogger">إظهار صاحب الترقية</string>
<string name="action_open_media_n">افتح الوسيط #%d</string> <string name="action_open_media_n">افتح الوسيط #%d</string>
<string name="download_media">تنزيل الوسائط</string> <string name="download_media">نزّل الوسائط</string>
<string name="downloading_media">جارٍ تنزيل الوسائط</string> <string name="downloading_media">جارٍ تنزيل الوسائط</string>
<string name="dialog_redraft_toot_warning">هل تريد حذف وإعادة صياغة هذا التبويق؟</string> <string name="dialog_redraft_toot_warning">هل تريد حذف وإعادة صياغة هذا التبويق؟</string>
@ -378,7 +378,7 @@
<string name="poll_ended_created">لقد انتهى استطلاع رأي قمتَ بإنشائه</string> <string name="poll_ended_created">لقد انتهى استطلاع رأي قمتَ بإنشائه</string>
<string name="pref_title_notification_filter_poll">انتهت استطلاعات الرأي</string> <string name="pref_title_notification_filter_poll">تنتهي استطلاعات الرأي</string>
<plurals name="favs"> <plurals name="favs">
<item quantity="zero"><b>%1$s</b>" مفضلة"</item> <item quantity="zero"><b>%1$s</b>" مفضلة"</item>
<item quantity="one"><b>%1$s</b> مفضلة</item> <item quantity="one"><b>%1$s</b> مفضلة</item>
@ -457,7 +457,7 @@
<string name="filter_dialog_whole_word">الكلمة كاملة</string> <string name="filter_dialog_whole_word">الكلمة كاملة</string>
<string name="description_poll">استطلاع رأي بالخيارات: %1$s, %2$s, %3$s, %4$s; %5$s</string> <string name="description_poll">استطلاع رأي بالخيارات: %1$s, %2$s, %3$s, %4$s; %5$s</string>
<string name="mute_domain_warning">هل أنت متأكد من أنك تريد حجب كافة %s ؟ سوف لن يكون باستطاعتك رؤية أي محتوى قادم من هذا النطاق بعد الآن ، لا في الخيوط الزمنية العامة ولا في إخطاراتك. سيتم إزالة متابِعيك الذين هم على هذا النطاق.</string> <string name="mute_domain_warning">هل أنت متأكد من أنك تريد حجب كافة %s؟ سوف لن يكون باستطاعتك رؤية أي محتوى قادم من هذا النطاق بعد الآن ، لا في الخيوط الزمنية العامة ولا في إخطاراتك. سيتم إزالة متابِعيك الذين هم على هذا النطاق.</string>
<string name="report_description_1">سيتم إرسال التقرير إلى مشرفي خادمك. يمكنك تقديم تفسير عن سبب الإبلاغ عن الحساب أدناه:</string> <string name="report_description_1">سيتم إرسال التقرير إلى مشرفي خادمك. يمكنك تقديم تفسير عن سبب الإبلاغ عن الحساب أدناه:</string>
<string name="report_description_remote_instance">هذا الحساب ينتسب إلى خادم آخر. هل تريد إرسال نسخة مجهولة من التقرير إلى هناك أيضا؟</string> <string name="report_description_remote_instance">هذا الحساب ينتسب إلى خادم آخر. هل تريد إرسال نسخة مجهولة من التقرير إلى هناك أيضا؟</string>
@ -501,4 +501,6 @@
<string name="no_scheduled_status">ليس لديك أية منشورات مُبرمَجة للنشر.</string> <string name="no_scheduled_status">ليس لديك أية منشورات مُبرمَجة للنشر.</string>
<string name="error_audio_upload_size">يجب أن يكون حجم الملفات الصوتية أقل مِن 40 ميغابايت.</string> <string name="error_audio_upload_size">يجب أن يكون حجم الملفات الصوتية أقل مِن 40 ميغابايت.</string>
</resources> <string name="warning_scheduling_interval">تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق.</string>
</resources>

View File

@ -534,4 +534,7 @@
<string name="no_scheduled_status">No tens cap estat planificat.</string> <string name="no_scheduled_status">No tens cap estat planificat.</string>
</resources> <string name="error_audio_upload_size">Els fitxers d\'àudio han de ser més petits de 40MB.</string>
<string name="no_saved_status">No tens cap esborrany.</string>
<string name="warning_scheduling_interval">L\'interval mínim de planificació a Mastodon és de 5 minuts.</string>
</resources>

View File

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

View File

@ -469,4 +469,9 @@
<string name="list">Listo</string> <string name="list">Listo</string>
<string name="post_lookup_error_format">Eraro dum elserĉo de la mesaĝo %s</string> <string name="post_lookup_error_format">Eraro dum elserĉo de la mesaĝo %s</string>
</resources> <string name="error_audio_upload_size">Aŭdia dosiero devas esti malpli ol 40MB.</string>
<string name="gradient_for_media">Montri buntajn transirojn por kaŝitaj aŭdovidaĵoj</string>
<string name="no_saved_status">Vi ne havas iun ajn malneton.</string>
<string name="no_scheduled_status">Vi ne havas iun ajn planitan mesaĝon.</string>
</resources>

View File

@ -4,7 +4,7 @@
<string name="error_network">¡Se ha producido un error de red! ¡Por favor, comprueba tu conexión e inténtalo de nuevo!</string> <string name="error_network">¡Se ha producido un error de red! ¡Por favor, comprueba tu conexión e inténtalo de nuevo!</string>
<string name="error_empty">Este campo no puede estar vacío.</string> <string name="error_empty">Este campo no puede estar vacío.</string>
<string name="error_invalid_domain">Nombre de dominio incorrecto</string> <string name="error_invalid_domain">Nombre de dominio incorrecto</string>
<string name="error_failed_app_registration">Inicio de sesión fallido.</string> <string name="error_failed_app_registration">Fallo de autenticación con esta instancia.</string>
<string name="error_no_web_browser_found">No se ha encontrado ningún navegador web.</string> <string name="error_no_web_browser_found">No se ha encontrado ningún navegador web.</string>
<string name="error_authorization_unknown">Ocurrió un error de autorización no identificado.</string> <string name="error_authorization_unknown">Ocurrió un error de autorización no identificado.</string>
<string name="error_authorization_denied">La autorización falló.</string> <string name="error_authorization_denied">La autorización falló.</string>
@ -474,9 +474,10 @@
<string name="select_list_title">Seleccionar lista</string> <string name="select_list_title">Seleccionar lista</string>
<string name="list">Lista</string> <string name="list">Lista</string>
<string name="error_audio_upload_size">Los ficheros de audio deben ser menores de 40MB.</string> <string name="error_audio_upload_size">Los ficheros de audio deben ser menores de 40MB.</string>
<string name="gradient_for_media">Mostrar degradados coloridos para el contenido multimedia oculto.</string> <string name="gradient_for_media">Mostrar degradados coloridos para el contenido multimedia oculto</string>
<string name="no_saved_status">No tienes ningún borrador.</string> <string name="no_saved_status">No tienes ningún borrador.</string>
<string name="no_scheduled_status">No tienes ningún estado programado.</string> <string name="no_scheduled_status">No tienes ningún estado programado.</string>
</resources> <string name="warning_scheduling_interval">Mastodon tiene un intervalo de programación mínimo de 5 minutos.</string>
</resources>

View File

@ -39,7 +39,7 @@
<string name="title_saved_toot">Brouillons</string> <string name="title_saved_toot">Brouillons</string>
<string name="title_licenses">Licences</string> <string name="title_licenses">Licences</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s a boosté</string> <string name="status_boosted_format">%s a partagé</string>
<string name="status_sensitive_media_title">Contenu sensible</string> <string name="status_sensitive_media_title">Contenu sensible</string>
<string name="status_media_hidden_title">Média caché</string> <string name="status_media_hidden_title">Média caché</string>
<string name="status_sensitive_media_directions">Cliquer pour voir</string> <string name="status_sensitive_media_directions">Cliquer pour voir</string>
@ -49,15 +49,15 @@
<string name="status_content_show_less">Replier</string> <string name="status_content_show_less">Replier</string>
<string name="message_empty">Rien ici.</string> <string name="message_empty">Rien ici.</string>
<string name="footer_empty">Il ny a aucun pouet pour le moment.\nGlissez vers le bas pour actualiser !</string> <string name="footer_empty">Il ny a aucun pouet pour le moment.\nGlissez vers le bas pour actualiser !</string>
<string name="notification_reblog_format">%s a boosté votre pouet</string> <string name="notification_reblog_format">%s a partagé votre pouet</string>
<string name="notification_favourite_format">%s a ajouté votre pouet à ses favoris</string> <string name="notification_favourite_format">%s a ajouté votre pouet à ses favoris</string>
<string name="notification_follow_format">%s vous suit</string> <string name="notification_follow_format">%s vous suit</string>
<string name="report_username_format">Signaler @%s</string> <string name="report_username_format">Signaler @%s</string>
<string name="report_comment_hint">Commentaires additonnels ?</string> <string name="report_comment_hint">Commentaires additonnels ?</string>
<string name="action_quick_reply">Réponse rapide</string> <string name="action_quick_reply">Réponse rapide</string>
<string name="action_reply">Répondre</string> <string name="action_reply">Répondre</string>
<string name="action_reblog">Booster</string> <string name="action_reblog">Partager</string>
<string name="action_unreblog">Supprimer le boost</string> <string name="action_unreblog">Annuler le partage</string>
<string name="action_favourite">Favori</string> <string name="action_favourite">Favori</string>
<string name="action_unfavourite">Supprimer le favori</string> <string name="action_unfavourite">Supprimer le favori</string>
<string name="action_more">Plus</string> <string name="action_more">Plus</string>
@ -69,8 +69,8 @@
<string name="action_unfollow">Ne plus suivre</string> <string name="action_unfollow">Ne plus suivre</string>
<string name="action_block">Bloquer</string> <string name="action_block">Bloquer</string>
<string name="action_unblock">Débloquer</string> <string name="action_unblock">Débloquer</string>
<string name="action_hide_reblogs">Cacher les boosts</string> <string name="action_hide_reblogs">Cacher les partages</string>
<string name="action_show_reblogs">Montrer les boosts</string> <string name="action_show_reblogs">Montrer les partages</string>
<string name="action_report">Signaler</string> <string name="action_report">Signaler</string>
<string name="action_delete">Supprimer</string> <string name="action_delete">Supprimer</string>
<string name="action_send">POUET</string> <string name="action_send">POUET</string>
@ -109,8 +109,8 @@
<string name="action_links">Liens</string> <string name="action_links">Liens</string>
<string name="action_mentions">Mentions</string> <string name="action_mentions">Mentions</string>
<string name="action_hashtags">Hashtags</string> <string name="action_hashtags">Hashtags</string>
<string name="action_open_reblogger">Afficher lauteur·rice du boost</string> <string name="action_open_reblogger">Afficher lauteur·rice du partage</string>
<string name="action_open_reblogged_by">Afficher les boosts</string> <string name="action_open_reblogged_by">Montrer les partages</string>
<string name="action_open_faved_by">Montrer les favoris</string> <string name="action_open_faved_by">Montrer les favoris</string>
<string name="title_hashtags_dialog">Hashtags</string> <string name="title_hashtags_dialog">Hashtags</string>
<string name="title_mentions_dialog">Mentions</string> <string name="title_mentions_dialog">Mentions</string>
@ -170,7 +170,7 @@
<string name="pref_title_notification_filters">Me notifier lorsque</string> <string name="pref_title_notification_filters">Me notifier lorsque</string>
<string name="pref_title_notification_filter_mentions">on me mentionne</string> <string name="pref_title_notification_filter_mentions">on me mentionne</string>
<string name="pref_title_notification_filter_follows">on me suit</string> <string name="pref_title_notification_filter_follows">on me suit</string>
<string name="pref_title_notification_filter_reblogs">mes messages sont boostés</string> <string name="pref_title_notification_filter_reblogs">mes pouets sont partagés</string>
<string name="pref_title_notification_filter_favourites">mes messages sont mis en favoris</string> <string name="pref_title_notification_filter_favourites">mes messages sont mis en favoris</string>
<string name="pref_title_appearance_settings">Apparence</string> <string name="pref_title_appearance_settings">Apparence</string>
<string name="pref_title_app_theme">Thème de lapplication</string> <string name="pref_title_app_theme">Thème de lapplication</string>
@ -187,7 +187,7 @@
<string name="pref_title_language">Langue</string> <string name="pref_title_language">Langue</string>
<string name="pref_title_status_filter">Filtrage des fils</string> <string name="pref_title_status_filter">Filtrage des fils</string>
<string name="pref_title_status_tabs">Onglets</string> <string name="pref_title_status_tabs">Onglets</string>
<string name="pref_title_show_boosts">Afficher les boosts</string> <string name="pref_title_show_boosts">Montrer les partages</string>
<string name="pref_title_show_replies">Afficher les réponses</string> <string name="pref_title_show_replies">Afficher les réponses</string>
<string name="pref_title_show_media_preview">Montrer les miniatures des médias</string> <string name="pref_title_show_media_preview">Montrer les miniatures des médias</string>
<string name="pref_title_proxy_settings">Proxy</string> <string name="pref_title_proxy_settings">Proxy</string>
@ -213,7 +213,7 @@
<string name="notification_follow_name">Nouveaux abonnés</string> <string name="notification_follow_name">Nouveaux abonnés</string>
<string name="notification_follow_description">Notifications pour les nouveaux abonnés</string> <string name="notification_follow_description">Notifications pour les nouveaux abonnés</string>
<string name="notification_boost_name">Partages</string> <string name="notification_boost_name">Partages</string>
<string name="notification_boost_description">Notifications quand vos pouets sont boostés</string> <string name="notification_boost_description">Notifications quand vos pouets sont partagés</string>
<string name="notification_favourite_name">Favoris</string> <string name="notification_favourite_name">Favoris</string>
<string name="notification_favourite_description">Notifications quand vos pouets sont mis en favoris</string> <string name="notification_favourite_description">Notifications quand vos pouets sont mis en favoris</string>
<string name="notification_mention_format">%s vous a mentionné</string> <string name="notification_mention_format">%s vous a mentionné</string>
@ -316,8 +316,8 @@
<string name="download_failed">Échec du téléchargement</string> <string name="download_failed">Échec du téléchargement</string>
<string name="profile_badge_bot_text">Robot</string> <string name="profile_badge_bot_text">Robot</string>
<string name="account_moved_description">%1$s a déménagé vers :</string> <string name="account_moved_description">%1$s a déménagé vers :</string>
<string name="reblog_private">Booster vers laudience originale</string> <string name="reblog_private">Partager à laudience originale</string>
<string name="unreblog_private">Ne plus booster</string> <string name="unreblog_private">Annuler le partage</string>
<string name="license_description">Yuito contient du code et des ressources issus des projets open source suivants :</string> <string name="license_description">Yuito contient du code et des ressources issus des projets open source suivants :</string>
<string name="license_apache_2">Sous licence Apache (copie ci-dessous)</string> <string name="license_apache_2">Sous licence Apache (copie ci-dessous)</string>
<string name="license_cc_by_4">CC-BY 4.0</string> <string name="license_cc_by_4">CC-BY 4.0</string>
@ -335,10 +335,10 @@
<item quantity="other"><b>%1$s</b> Favoris</item> <item quantity="other"><b>%1$s</b> Favoris</item>
</plurals> </plurals>
<plurals name="reblogs"> <plurals name="reblogs">
<item quantity="one">&lt;b&gt;%s&lt;/b&gt; Boost</item> <item quantity="one"><b>%s</b> Partage</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Boosts</item> <item quantity="other"><b>%s</b> Partages</item>
</plurals> </plurals>
<string name="title_reblogged_by">Boosté par</string> <string name="title_reblogged_by">Partagé par</string>
<string name="title_favourited_by">Mis en favoris par</string> <string name="title_favourited_by">Mis en favoris par</string>
<string name="conversation_1_recipients">%1$s</string> <string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s et %2$s</string> <string name="conversation_2_recipients">%1$s et %2$s</string>
@ -371,7 +371,7 @@
<string name="notifications_apply_filter">Filtrer</string> <string name="notifications_apply_filter">Filtrer</string>
<string name="filter_apply">Appliquer</string> <string name="filter_apply">Appliquer</string>
<string name="compose_shortcut_long_label">Écrire un pouet</string> <string name="compose_shortcut_long_label">Rédiger un pouet</string>
<string name="compose_shortcut_short_label">Écrire</string> <string name="compose_shortcut_short_label">Écrire</string>
<string name="pref_title_bot_overlay">Afficher l\'indicateur de robots</string> <string name="pref_title_bot_overlay">Afficher l\'indicateur de robots</string>
@ -486,4 +486,6 @@
<string name="no_saved_status">Vous navez aucun brouillon.</string> <string name="no_saved_status">Vous navez aucun brouillon.</string>
<string name="no_scheduled_status">Vous navez aucun pouet planifié.</string> <string name="no_scheduled_status">Vous navez aucun pouet planifié.</string>
<string name="warning_scheduling_interval">Lintervalle minimum de planification sur Mastodon est de 5 minutes.</string>
</resources> </resources>

View File

@ -508,4 +508,6 @@
<string name="no_scheduled_status">Þú ert ekki með neinar áætlaðar stöðufærslur.</string> <string name="no_scheduled_status">Þú ert ekki með neinar áætlaðar stöðufærslur.</string>
<string name="error_audio_upload_size">Hljóðskrár verða að vera minni en 40MB.</string> <string name="error_audio_upload_size">Hljóðskrár verða að vera minni en 40MB.</string>
</resources> <string name="warning_scheduling_interval">Mastodon er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir.</string>
</resources>

View File

@ -420,4 +420,10 @@
<string name="pref_title_show_notifications_filter">通知フィルターを表示</string> <string name="pref_title_show_notifications_filter">通知フィルターを表示</string>
<string name="action_reset_schedule">リセット</string>
<string name="error_audio_upload_size">音声ファイルは40MB未満にしてください。</string>
<string name="title_bookmarks">ブックマーク</string>
<string name="action_bookmark">ブックマーク</string>
<string name="action_edit">編集</string>
<string name="action_view_bookmarks">ブックマーク</string>
</resources> </resources>

View File

@ -1,22 +1,22 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<resources><string name="action_login">Qqen γer Maṣṭudun</string> <resources><string name="action_login">Qqen ɣer Maṣṭudun</string>
<string name="title_favourites">Ismenyifen</string> <string name="title_favourites">Ismenyifen</string>
<string name="title_saved_toot">Irewwayen</string> <string name="title_saved_toot">Irewwayen</string>
<string name="action_logout">Ffeγ</string> <string name="action_logout">Ffeɣ</string>
<string name="action_view_preferences">Iγewwaṛen</string> <string name="action_view_preferences">Iɣewwaṛen</string>
<string name="action_view_account_preferences">Iγewwaṛen n umiḍan</string> <string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string>
<string name="action_edit_profile">Ẓreg amaγnu</string> <string name="action_edit_profile">Ẓreg amaɣnu</string>
<string name="action_search">Nadi</string> <string name="action_search">Nadi</string>
<string name="about_title_activity">Γef</string> <string name="about_title_activity">Ɣef</string>
<string name="action_lists">Umuγen</string> <string name="action_lists">Umuɣen</string>
<string name="title_lists">Umuγen</string> <string name="title_lists">Umuɣen</string>
<string name="error_compose_character_limit">Tijewwiqt-ik aṭas i γuzzifet!</string> <string name="error_compose_character_limit">Tijewwiqt-ik aṭas i ɣuzzifet!</string>
<string name="title_home">Agejdan</string> <string name="title_home">Agejdan</string>
<string name="title_tab_preferences">Iccaren</string> <string name="title_tab_preferences">Iccaren</string>
<string name="title_view_thread">Tijewwiqt</string> <string name="title_view_thread">Tijewwiqt</string>
<string name="title_statuses">Iznan</string> <string name="title_statuses">Iznan</string>
<string name="title_statuses_with_replies">S tririyin</string> <string name="title_statuses_with_replies">S tririyin</string>
<string name="title_edit_profile">Ẓreg amaγnu-ik</string> <string name="title_edit_profile">Ẓreg amaɣnu-ik</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_content_warning_show_more">Zeṛ ugar</string> <string name="status_content_warning_show_more">Zeṛ ugar</string>
<string name="status_content_warning_show_less">Zeṛ kra kan</string> <string name="status_content_warning_show_less">Zeṛ kra kan</string>
@ -35,13 +35,13 @@
<string name="action_send_public">JEWWEQ!</string> <string name="action_send_public">JEWWEQ!</string>
<string name="action_retry">Ɛreḍ tikkelt-nniḍen</string> <string name="action_retry">Ɛreḍ tikkelt-nniḍen</string>
<string name="action_close">Derreɛ</string> <string name="action_close">Derreɛ</string>
<string name="action_view_profile">Amaγnu</string> <string name="action_view_profile">Amaɣnu</string>
<string name="action_view_favourites">Ismenyifen</string> <string name="action_view_favourites">Ismenyifen</string>
<string name="action_open_in_web">Ldi deg uminig</string> <string name="action_open_in_web">Ldi deg uminig</string>
<string name="action_share">Bḍu</string> <string name="action_share">Bḍu</string>
<string name="action_mute">Sgugem</string> <string name="action_mute">Sgugem</string>
<string name="action_access_saved_toot">Irewwayen</string> <string name="action_access_saved_toot">Irewwayen</string>
<string name="action_open_faved_by">Sken-ed ismenyifen</string> <string name="action_open_faved_by">Sken-d ismenyifen</string>
<string name="notification_favourite_name">Ismenyifen</string> <string name="notification_favourite_name">Ismenyifen</string>
<plurals name="favs"> <plurals name="favs">
@ -51,11 +51,11 @@
<string name="no_saved_status">Ur tesɛiḍ ara irewwayen.</string> <string name="no_saved_status">Ur tesɛiḍ ara irewwayen.</string>
<string name="error_generic">Tella-d tucḍa.</string> <string name="error_generic">Tella-d tucḍa.</string>
<string name="title_notifications">Tilγa</string> <string name="title_notifications">Tilɣa</string>
<string name="link_whats_an_instance">D acu i ttummant\?</string> <string name="link_whats_an_instance">D acu i ttummant\?</string>
<string name="title_bookmarks">Ticraḍ</string> <string name="title_bookmarks">Ticraḍ</string>
<string name="action_bookmark">Rnu γer ticraḍ</string> <string name="action_bookmark">Rnu ɣer ticraḍ</string>
<string name="action_view_bookmarks">Ticraḍ</string> <string name="action_view_bookmarks">Ticraḍ</string>
<string name="action_mute_domain">Sgugem %s</string> <string name="action_mute_domain">Sgugem %s</string>
<string name="action_mention">Bder</string> <string name="action_mention">Bder</string>
@ -64,20 +64,20 @@
<string name="action_undo">Sefsex</string> <string name="action_undo">Sefsex</string>
<string name="action_emoji_keyboard">Anasiw n imujiyen</string> <string name="action_emoji_keyboard">Anasiw n imujiyen</string>
<string name="action_add_tab">Rnu iccer</string> <string name="action_add_tab">Rnu iccer</string>
<string name="action_copy_link">Nγel aseγwen</string> <string name="action_copy_link">Nɣel aseɣwen</string>
<string name="action_open_as">Ldi amzun d %s</string> <string name="action_open_as">Ldi amzun d %s</string>
<string name="action_share_as">Bḍu amzun d…</string> <string name="action_share_as">Bḍu amzun d…</string>
<string name="send_status_link_to">Bḍu aseγwen n tijewwiq s…</string> <string name="send_status_link_to">Bḍu aseɣwen n tijewwiq s…</string>
<string name="send_status_content_to">Bḍu tijewwiqt d…</string> <string name="send_status_content_to">Bḍu tijewwiqt d…</string>
<string name="hint_domain">Anta tummant\?</string> <string name="hint_domain">Anta tummant\?</string>
<string name="hint_compose">d-acu i gellan d amaynut\?</string> <string name="hint_compose">d-acu i gellan d amaynut\?</string>
<string name="hint_search">Nadi…</string> <string name="hint_search">Nadi…</string>
<string name="label_quick_reply">Tiririn…</string> <string name="label_quick_reply">Tiririn…</string>
<string name="label_avatar">Tugna n umaγnu</string> <string name="label_avatar">Tugna n umaɣnu</string>
<string name="dialog_download_image">Sider</string> <string name="dialog_download_image">Sider</string>
<string name="dialog_delete_toot_warning">Kkes tijewwiqt-a\?</string> <string name="dialog_delete_toot_warning">Kkes tijewwiqt-a\?</string>
<string name="pref_title_edit_notification_settings">Ẓreg tilγa</string> <string name="pref_title_edit_notification_settings">Ẓreg tilɣa</string>
<string name="pref_title_appearance_settings">Agrudem</string> <string name="pref_title_appearance_settings">Agrudem</string>
<string name="app_theme_light">Aceɛlal</string> <string name="app_theme_light">Aceɛlal</string>
<string name="app_theme_black">Aberkan</string> <string name="app_theme_black">Aberkan</string>
@ -88,7 +88,7 @@
<string name="notification_summary_medium">%1$s, %2$s, akked %3$s</string> <string name="notification_summary_medium">%1$s, %2$s, akked %3$s</string>
<string name="notification_summary_small">%1$s akked %2$s</string> <string name="notification_summary_small">%1$s akked %2$s</string>
<string name="about_tusky_version">Tusky %s</string> <string name="about_tusky_version">Tusky %s</string>
<string name="about_tusky_account">Amaγnu n Tusky</string> <string name="about_tusky_account">Amaɣnu n Tusky</string>
<string name="status_media_images">Tugniwin</string> <string name="status_media_images">Tugniwin</string>
<string name="status_media_video">Tibidyutin</string> <string name="status_media_video">Tibidyutin</string>
@ -109,17 +109,17 @@
<string name="pin_action">Senṭeḍ</string> <string name="pin_action">Senṭeḍ</string>
<string name="action_view_mutes">Imiḍanen yettwasgugmen</string> <string name="action_view_mutes">Imiḍanen yettwasgugmen</string>
<string name="action_view_blocks">Imiḍanen yettusḥebsen</string> <string name="action_view_blocks">Imiḍan yettwacekklen</string>
<string name="action_view_domain_mutes">Tiγula yettwaffren</string> <string name="action_view_domain_mutes">Tiɣula yettwaffren</string>
<string name="action_view_follow_requests">Isuturen n teḍfeṛt</string> <string name="action_view_follow_requests">Isuturen n teḍfeṛt</string>
<string name="action_view_media">Taγwalt</string> <string name="action_view_media">Taɣwalt</string>
<string name="notifications_clear">Sfeḍ</string> <string name="notifications_clear">Sfeḍ</string>
<string name="title_mutes">Imiḍanen yettwasgugmen</string> <string name="title_mutes">Imiḍanen yettwasgugmen</string>
<string name="title_blocks">Imiḍanen yettusḥebsen</string> <string name="title_blocks">Imiḍanen yettwacekklen</string>
<string name="title_domain_mutes">Tiγula yettwaffren</string> <string name="title_domain_mutes">Tiɣula yettwaffren</string>
<string name="title_follow_requests">Isuturen n teḍfeṛt</string> <string name="title_follow_requests">Isuturen n teḍfeṛt</string>
<string name="pref_title_notifications_enabled">Ẓreg tilγa</string> <string name="pref_title_notifications_enabled">Tilɣa</string>
<string name="title_media">Taγwalt</string> <string name="title_media">Taywalt</string>
<string name="action_remove">Kkes</string> <string name="action_remove">Kkes</string>
<string name="compose_shortcut_short_label">Azen</string> <string name="compose_shortcut_short_label">Azen</string>
@ -129,28 +129,28 @@
<string name="action_add_poll">Rnu assenqed</string> <string name="action_add_poll">Rnu assenqed</string>
<string name="action_photo_take">Ṭef tugna</string> <string name="action_photo_take">Ṭef tugna</string>
<string name="action_toggle_visibility">Timeẓriwt n tijewwaqt</string> <string name="action_toggle_visibility">Timeẓriwt n tijewwaqt</string>
<string name="action_schedule_toot">Sγiwes tijewwaqt-a</string> <string name="action_schedule_toot">Sɣiwes tijewwaqt-a</string>
<string name="status_share_content">Bḍu agbur n tijewwiqt-a</string> <string name="status_share_content">Bḍu agbur n tijewwiqt-a</string>
<string name="status_share_link">Bḍu aseγwen γer tijewwiqt</string> <string name="status_share_link">Bḍu aseɣwen ɣer tijewwiqt</string>
<string name="filter_addition_dialog_title">Rnu amsizdeg</string> <string name="filter_addition_dialog_title">Rnu amsizdeg</string>
<string name="filter_edit_dialog_title">Ẓreg amsizdeg</string> <string name="filter_edit_dialog_title">Ẓreg amsizdeg</string>
<string name="action_create_list">Snulfu-d umuγ</string> <string name="action_create_list">Snulfu-d umuɣ</string>
<string name="action_rename_list">Snifel isem n wumuγ</string> <string name="action_rename_list">Snifel isem n wumuɣ</string>
<string name="action_delete_list">Kkes umuγ-a</string> <string name="action_delete_list">Kkes umuɣ-a</string>
<string name="action_edit_list">Ẓreg umuγ-a</string> <string name="action_edit_list">Ẓreg umuɣ-a</string>
<string name="action_add_to_list">Rnu yiwen umiḍan γer tabdert</string> <string name="action_add_to_list">Rnu yiwen umiḍan ɣer wummuɣ</string>
<string name="action_remove_from_list">Kkes amiḍan seg wumuγ</string> <string name="action_remove_from_list">Kkes amiḍan seg wumuɣ</string>
<string name="profile_metadata_add">Rnu isefka</string> <string name="profile_metadata_add">Rnu isefka</string>
<string name="hint_list_name">Isem n wumuγ</string> <string name="hint_list_name">Isem n wumuɣ</string>
<string name="select_list_title">Fren tabdart</string> <string name="select_list_title">Fren tabdart</string>
<string name="list">Umuγ</string> <string name="list">Umuɣ</string>
<string name="notifications_apply_filter">Sizdeg</string> <string name="notifications_apply_filter">Sizdeg</string>
<string name="title_accounts">Imiḍanen</string> <string name="title_accounts">Imiḍanen</string>
<string name="add_poll_choice">Rnu yiwen wefran</string> <string name="add_poll_choice">Rnu yiwen wefran</string>
<string name="report_username_format">Ccetki γef @%s</string> <string name="report_username_format">Ccetki ɣef @%s</string>
<string name="action_report">Ccetki</string> <string name="action_report">Ccetki fell-as</string>
<string name="action_reject">Ggami</string> <string name="action_reject">Ggami</string>
<string name="download_image">Yessidired %1$s</string> <string name="download_image">Yessidired %1$s</string>
@ -159,16 +159,16 @@
<string name="login_connection">itteqqen…</string> <string name="login_connection">itteqqen…</string>
<string name="dialog_message_uploading_media">Issalay…</string> <string name="dialog_message_uploading_media">Issalay…</string>
<string name="pref_title_notification_filter_poll">fukken kran n wadγaren</string> <string name="pref_title_notification_filter_poll">fukken kran n wadɣaren</string>
<string name="pref_title_timeline_filters">Imzizdigen</string> <string name="pref_title_timeline_filters">Imzizdigen</string>
<string name="app_theme_auto">Akken yella yiṭij</string> <string name="app_theme_auto">Akken yella yiṭij</string>
<string name="pref_title_browser_settings">Iminig</string> <string name="pref_title_browser_settings">Iminig</string>
<string name="pref_title_show_replies">Sken-ed tiririyin</string> <string name="pref_title_show_replies">Sken-d tiririyin</string>
<string name="pref_title_http_proxy_settings">Apṛuksi HTTP</string> <string name="pref_title_http_proxy_settings">Apṛuksi HTTP</string>
<string name="pref_title_http_proxy_server">Tansa n upṛuksi HTTP</string> <string name="pref_title_http_proxy_server">Tansa n upṛuksi HTTP</string>
<string name="notification_follow_name">Imeḍfaṛen imaynuten</string> <string name="notification_follow_name">Imeḍfaṛen imaynuten</string>
<string name="notification_poll_name">Adγaren</string> <string name="notification_poll_name">Adɣaren</string>
<string name="notification_mention_format">Yuder-ik-id %s</string> <string name="notification_mention_format">Yuder-ik-id %s</string>
<string name="description_account_locked">Yettwargel umiḍan</string> <string name="description_account_locked">Yettwargel umiḍan</string>
@ -183,26 +183,26 @@
<string name="restart">Ales tanekra</string> <string name="restart">Ales tanekra</string>
<string name="download_failed">Tuccḍa n usider</string> <string name="download_failed">Tuccḍa n usider</string>
<string name="account_moved_description">Igujj %1$s γer:</string> <string name="account_moved_description">Igujj %1$s ɣer:</string>
<string name="unpin_action">Kkes asenṭeḍ</string> <string name="unpin_action">Kkes asenṭeḍ</string>
<string name="conversation_1_recipients">%1$s</string> <string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s akked %2$s</string> <string name="conversation_2_recipients">%1$s akked %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s akked %3$d nniḍen</string> <string name="conversation_more_recipients">%1$s, %2$s akked %3$d nniḍen</string>
<string name="compose_shortcut_long_label">Aru tijewwiqt</string> <string name="compose_shortcut_long_label">Aru tijewwiqt</string>
<string name="poll_info_format"> <!-- 15 n wadγaren • 1 n wesrag id yeqqimen --> %1$s • %2$s</string> <string name="poll_info_format"> <!-- 15 n wadɣaren • 1 n wesrag id yeqqimen --> %1$s • %2$s</string>
<plurals name="poll_info_votes"> <plurals name="poll_info_votes">
<item quantity="one">%s wedγar</item> <item quantity="one">%s n wedɣar</item>
<item quantity="other">%s n yedγaren</item> <item quantity="other">%s n yedɣaren</item>
</plurals> </plurals>
<string name="poll_info_time_relative">%s id yugran</string> <string name="poll_info_time_relative">%s id yugran</string>
<string name="poll_info_time_absolute">ad ifak deg %s</string> <string name="poll_info_time_absolute">ad ifak deg %s</string>
<string name="poll_info_closed">ifuk</string> <string name="poll_info_closed">ifuk</string>
<string name="poll_vote">Dγer</string> <string name="poll_vote">Dɣer</string>
<string name="poll_ended_voted">Ifuk, tura kan, yiwen wedγar t tteki-iḍ degs</string> <string name="poll_ended_voted">Ifuk, tura kan, yiwen wedɣar t tteki-iḍ degs</string>
<string name="poll_ended_created">Ifukk yiwen wedγar id snulfaḍ</string> <string name="poll_ended_created">Ifukk yiwen wedɣar id snulfaḍ</string>
<plurals name="poll_timespan_days"> <plurals name="poll_timespan_days">
<item quantity="one">%d n wass</item> <item quantity="one">%d n wass</item>
@ -214,18 +214,18 @@
</plurals> </plurals>
<plurals name="poll_timespan_minutes"> <plurals name="poll_timespan_minutes">
<item quantity="one">%d n tasdidt</item> <item quantity="one">%d n tasdidt</item>
<item quantity="other">%d n tesdidin</item> <item quantity="other">%d n tisdidin</item>
</plurals> </plurals>
<string name="button_continue">Kemmel</string> <string name="button_continue">Kemmel</string>
<string name="button_back">Uγal</string> <string name="button_back">Uɣal</string>
<string name="failed_report">Tella-d tuccḍa deg ccetki</string> <string name="failed_report">Tella-d tuccḍa deg ccetki</string>
<string name="failed_search">Tucḍa n unadi</string> <string name="failed_search">Tucḍa n unadi</string>
<string name="create_poll_title">Assenqed</string> <string name="create_poll_title">Assenqed</string>
<string name="poll_duration_5_min">5 n tasditin</string> <string name="poll_duration_5_min">5 n tisdidin</string>
<string name="poll_duration_30_min">30 n tasditin</string> <string name="poll_duration_30_min">30 n tisdidin</string>
<string name="poll_duration_1_hour">1 n wesrag</string> <string name="poll_duration_1_hour">1 n usrag</string>
<string name="poll_duration_6_hours">6 n wesragen</string> <string name="poll_duration_6_hours">6 n isragen</string>
<string name="poll_duration_1_day">1 n wass</string> <string name="poll_duration_1_day">1 n wass</string>
<string name="poll_duration_3_days">3 n wussan</string> <string name="poll_duration_3_days">3 n wussan</string>
<string name="poll_duration_7_days">7 n wussan</string> <string name="poll_duration_7_days">7 n wussan</string>
@ -233,30 +233,70 @@
<string name="title_follows">Ig ṭafaṛ</string> <string name="title_follows">Ig ṭafaṛ</string>
<string name="title_followers">Imeḍfaṛen</string> <string name="title_followers">Imeḍfaṛen</string>
<string name="hint_search_people_list">Nadi γef medden i teṭafareḍ</string> <string name="hint_search_people_list">Nadi ɣef medden i teṭafareḍ</string>
<string name="description_visiblity_private">Imeḍfaṛen</string> <string name="description_visiblity_private">Imeḍfaṛen</string>
<string name="action_links">Iseγwan</string> <string name="action_links">Iseɣwan</string>
<string name="action_mentions">Tibdarin</string> <string name="action_mentions">Tibdarin</string>
<string name="title_mentions_dialog">Tibdarin</string> <string name="title_mentions_dialog">Tibdarin</string>
<string name="title_links_dialog">Iseγwan</string> <string name="title_links_dialog">Iseɣwan</string>
<string name="confirmation_reported">Yettwaceyyaɛ!</string> <string name="confirmation_reported">Yettwaceyyaɛ!</string>
<string name="status_sent">Yettwaceyyaɛ!</string> <string name="status_sent">Yettwaceyyaɛ!</string>
<string name="search_no_results">Ula d yiwen n ugmuḍ</string> <string name="search_no_results">Ula d yiwen n ugmuḍ</string>
<string name="post_privacy_followers_only">I yimeḍfaṛen kan</string> <string name="post_privacy_followers_only">I yimeḍfaṛen kan</string>
<string name="pref_status_text_size">Teγzi n weḍṛis</string> <string name="pref_status_text_size">Teɣzi n weḍṛis</string>
<string name="about_powered_by_tusky">Yettwamdemmar s Tusky</string> <string name="about_powered_by_tusky">Yettwamdemmar s Tusky</string>
<string name="about_project_site">Asmel Web n usenfaṛ: <string name="about_project_site">Asmel Web n usenfaṛ:
\n https://tusky.app</string> \n https://tusky.app</string>
<string name="abbreviated_hours_ago">%dasr</string> <string name="abbreviated_hours_ago">%dsr</string>
<string name="abbreviated_minutes_ago">%dtas</string> <string name="abbreviated_minutes_ago">%dtsd</string>
<string name="abbreviated_seconds_ago">%dtasn</string> <string name="abbreviated_seconds_ago">%dtsn</string>
<string name="compose_save_draft">Sekles amzun d arewway\?</string> <string name="compose_save_draft">Sekles amzun d arewway\?</string>
<string name="later">Ticki</string> <string name="later">Ticki</string>
<string name="profile_badge_bot_text">Aṛubut</string> <string name="profile_badge_bot_text">Aṛubut</string>
<string name="description_status_bookmarked">Yettwarna γer ticṛad</string> <string name="description_status_bookmarked">Yettwarna ɣer ticṛad</string>
<string name="abbreviated_in_hours">deg %dsr</string>
<string name="abbreviated_in_minutes">deg %dtsd</string>
<string name="abbreviated_in_seconds">deg %dtsn</string>
<plurals name="poll_timespan_seconds">
<item quantity="one">%d n tasint</item>
<item quantity="other">%d n tasinin</item>
</plurals>
<string name="status_sensitive_media_title">Agbur amḥulfu</string>
<string name="pref_default_media_sensitivity">Creḍ allal n teywalt amzun d amḥulfu</string>
<string name="action_reset_schedule">Wennez tikkelt-nniḍen</string>
<string name="error_media_upload_sending">Asali ur yeddi ara.</string>
<string name="error_sender_account_gone">Tuccḍa deg tuzna n tijewwiqt.</string>
<string name="title_public_local">Adigan</string>
<string name="title_licenses">Turagin</string>
<string name="status_boosted_format">Yebḍa-t %s</string>
<string name="notification_reblog_format">%s Y·Tebḍa tijewwiqt-ik·im</string>
<string name="notification_favourite_format">%s Y·Terna tijewwiqt-ik·im ɣer imenyafen-is</string>
<string name="action_quick_reply">Tiririt taruradt</string>
<string name="action_reblog">Bḍu</string>
<string name="action_unreblog">Kkes beṭu</string>
<string name="action_hide_reblogs">Ffer beṭuyat</string>
<string name="action_show_reblogs">Sken-d beṭuyat</string>
<string name="action_unmute">Ur sgugum ara</string>
<string name="action_accept">Ddeg</string>
<string name="action_hashtags">Ihacṭagen</string>
<string name="action_open_reblogged_by">Sken-d beṭuyat</string>
<string name="title_hashtags_dialog">Ihacṭagen</string>
<string name="confirmation_unmuted">Aseqdac nni ur yettwasgugem ara tura</string>
<string name="confirmation_domain_unmuted">%s ur yettwaffer ara</string>
<string name="hint_note">Assisen</string>
<string name="label_header">Tugna n yiɣef n umaɣnu</string>
<string name="error_empty">Ur ilaq ara ad yili d ilem.</string>
<string name="action_block">Cekkel</string>
<string name="action_unblock">Kkes tacekkalt</string>
<string name="confirmation_unblocked">Tettwakkes tacekkalt ɣef umiḍan-nni</string>
</resources> </resources>

View File

@ -520,4 +520,6 @@
<string name="no_saved_status">Du har ikke lagret noen kladder.</string> <string name="no_saved_status">Du har ikke lagret noen kladder.</string>
<string name="error_audio_upload_size">Lydfiler må være mindre enn 40MB.</string> <string name="error_audio_upload_size">Lydfiler må være mindre enn 40MB.</string>
</resources> <string name="warning_scheduling_interval">Mastodon har et minimums planleggingsinterval på 5 minutter.</string>
</resources>

View File

@ -491,4 +491,5 @@
<string name="no_saved_status">Avètz pas cap de borrolhon.</string> <string name="no_saved_status">Avètz pas cap de borrolhon.</string>
<string name="no_scheduled_status">Avètz pas cap de tut planificat.</string> <string name="no_scheduled_status">Avètz pas cap de tut planificat.</string>
</resources> <string name="warning_scheduling_interval">Linterval minimum de planificacion sus Mastodon e de 5 minutas.</string>
</resources>

View File

@ -498,4 +498,6 @@
<string name="no_saved_status">Nie masz żadnych szkiców.</string> <string name="no_saved_status">Nie masz żadnych szkiców.</string>
<string name="no_scheduled_status">Nie masz żadnych zaplanowanych wpisów.</string> <string name="no_scheduled_status">Nie masz żadnych zaplanowanych wpisów.</string>
<string name="warning_scheduling_interval">Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania.</string>
</resources> </resources>

View File

@ -486,4 +486,5 @@
<string name="error_audio_upload_size">Áudios devem ser menores que 40MB.</string> <string name="error_audio_upload_size">Áudios devem ser menores que 40MB.</string>
<string name="no_saved_status">Sem rascunhos.</string> <string name="no_saved_status">Sem rascunhos.</string>
<string name="warning_scheduling_interval">Mastodon possui um intervalo mínimo de 5 minutos para agendar.</string>
</resources> </resources>

View File

@ -546,4 +546,13 @@
<string name="description_status_bookmarked">Добавлено в закладки</string> <string name="description_status_bookmarked">Добавлено в закладки</string>
<string name="select_list_title">Выбрать список</string> <string name="select_list_title">Выбрать список</string>
<string name="list">Список</string> <string name="list">Список</string>
</resources> <string name="error_audio_upload_size">Аудиофайлы должны быть меньше 40МБ.</string>
<string name="gradient_for_media">Показывать цветные градиенты для скрытых медиа</string>
<string name="post_lookup_error_format">Ошибка поиска поста %s</string>
<string name="no_saved_status">У вас нет черновиков.</string>
<string name="no_scheduled_status">У вас нет запланированных постов.</string>
<string name="warning_scheduling_interval">Минимальный интервал планирования в Mastodon составляет 5 минут.</string>
</resources>

View File

@ -20,7 +20,7 @@
<string name="error_media_upload_sending">Uppladdningen misslyckades.</string> <string name="error_media_upload_sending">Uppladdningen misslyckades.</string>
<string name="error_sender_account_gone">Kunde inte skicka toot.</string> <string name="error_sender_account_gone">Kunde inte skicka toot.</string>
<string name="title_home">Hem</string> <string name="title_home">Hem</string>
<string name="title_notifications">Notifikationer</string> <string name="title_notifications">Aviseringar</string>
<string name="title_public_local">Lokalt</string> <string name="title_public_local">Lokalt</string>
<string name="title_public_federated">Federerat</string> <string name="title_public_federated">Federerat</string>
<string name="title_direct_messages">Direkta meddelanden</string> <string name="title_direct_messages">Direkta meddelanden</string>
@ -158,15 +158,15 @@
<string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string> <string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string>
<string name="visibility_private">Enbart-följare: Ses enbart av följare</string> <string name="visibility_private">Enbart-följare: Ses enbart av följare</string>
<string name="visibility_direct">Direkt: Skicka endast till nämnda användare</string> <string name="visibility_direct">Direkt: Skicka endast till nämnda användare</string>
<string name="pref_title_edit_notification_settings">Notifikationer</string> <string name="pref_title_edit_notification_settings">Aviseringar</string>
<string name="pref_title_notifications_enabled">Notifikationer</string> <string name="pref_title_notifications_enabled">Aviseringar</string>
<string name="pref_title_notification_alerts">Alarm</string> <string name="pref_title_notification_alerts">Alarm</string>
<string name="pref_title_notification_alert_sound">Meddela med ljud</string> <string name="pref_title_notification_alert_sound">Meddela med ljud</string>
<string name="pref_title_notification_alert_vibrate">Meddela med vibration</string> <string name="pref_title_notification_alert_vibrate">Meddela med vibration</string>
<string name="pref_title_notification_alert_light">Notifieringar med LED</string> <string name="pref_title_notification_alert_light">Notifieringar med LED</string>
<string name="pref_title_notification_filters">Meddela mig när</string> <string name="pref_title_notification_filters">Meddela mig när</string>
<string name="pref_title_notification_filter_mentions">omnämnd</string> <string name="pref_title_notification_filter_mentions">omnämnd</string>
<string name="pref_title_notification_filter_follows">följande</string> <string name="pref_title_notification_filter_follows">nya följare</string>
<string name="pref_title_notification_filter_reblogs">mina inlägg är knuffade</string> <string name="pref_title_notification_filter_reblogs">mina inlägg är knuffade</string>
<string name="pref_title_notification_filter_favourites">mina inlägg är favoriserade</string> <string name="pref_title_notification_filter_favourites">mina inlägg är favoriserade</string>
<string name="pref_title_appearance_settings">Utseende</string> <string name="pref_title_appearance_settings">Utseende</string>
@ -177,7 +177,7 @@
<string name="app_theme_light">Ljust</string> <string name="app_theme_light">Ljust</string>
<string name="app_theme_black">Svart</string> <string name="app_theme_black">Svart</string>
<string name="app_theme_auto">Automatiskt vid solnedgång</string> <string name="app_theme_auto">Automatiskt vid solnedgång</string>
<string name="app_theme_system">Använd systemdesign</string> <string name="app_theme_system">Använd system-tema</string>
<string name="pref_title_browser_settings">Webbläsare</string> <string name="pref_title_browser_settings">Webbläsare</string>
<string name="pref_title_custom_tabs">Använd Chrome-anpassade flikar</string> <string name="pref_title_custom_tabs">Använd Chrome-anpassade flikar</string>
<string name="pref_title_hide_follow_button">Dölj skriv-knappen vid skrollning</string> <string name="pref_title_hide_follow_button">Dölj skriv-knappen vid skrollning</string>
@ -206,13 +206,13 @@
<string name="status_text_size_large">Stor</string> <string name="status_text_size_large">Stor</string>
<string name="status_text_size_largest">Största</string> <string name="status_text_size_largest">Största</string>
<string name="notification_mention_name">Nya omnämnanden</string> <string name="notification_mention_name">Nya omnämnanden</string>
<string name="notification_mention_descriptions">Notifieringar om nya omnämnanden</string> <string name="notification_mention_descriptions">Aviseringar om nya omnämnanden</string>
<string name="notification_follow_name">Nya följare</string> <string name="notification_follow_name">Nya följare</string>
<string name="notification_follow_description">Notifieringar om nya följare</string> <string name="notification_follow_description">Aviseringar på nya följare</string>
<string name="notification_boost_name">Knuffar</string> <string name="notification_boost_name">Knuffar</string>
<string name="notification_boost_description">Notifieringar när dina toots blir knuffade</string> <string name="notification_boost_description">Aviseringar när dina toots blir knuffade</string>
<string name="notification_favourite_name">Favoriter</string> <string name="notification_favourite_name">Favoriter</string>
<string name="notification_favourite_description">Notifieringar när dina toots blir markerade som favoriter</string> <string name="notification_favourite_description">Aviseringar när dina toots blir markerade som favoriter</string>
<string name="notification_mention_format">%s omnämnde dig</string> <string name="notification_mention_format">%s omnämnde dig</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s och %4$d andra</string> <string name="notification_summary_large">%1$s, %2$s, %3$s och %4$d andra</string>
<string name="notification_summary_medium">%1$s, %2$s, och %3$s</string> <string name="notification_summary_medium">%1$s, %2$s, och %3$s</string>
@ -239,7 +239,7 @@
<string name="status_share_link">Dela länk till toot</string> <string name="status_share_link">Dela länk till toot</string>
<string name="status_media_images">Bilder</string> <string name="status_media_images">Bilder</string>
<string name="status_media_video">Video</string> <string name="status_media_video">Video</string>
<string name="state_follow_requested">Följarförfrågad</string> <string name="state_follow_requested">Följarförfrågan</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"--> <!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">om %dy</string> <string name="abbreviated_in_years">om %dy</string>
<string name="abbreviated_in_days">om %dd</string> <string name="abbreviated_in_days">om %dd</string>
@ -272,9 +272,9 @@
<string name="error_rename_list">Kunde inte byta namn på lista</string> <string name="error_rename_list">Kunde inte byta namn på lista</string>
<string name="error_delete_list">Kunde inte radera lista</string> <string name="error_delete_list">Kunde inte radera lista</string>
<string name="action_create_list">Skapa en lista</string> <string name="action_create_list">Skapa en lista</string>
<string name="action_rename_list">Byt namn</string> <string name="action_rename_list">Byt namn på listan</string>
<string name="action_delete_list">Ta bort</string> <string name="action_delete_list">Ta bort listan</string>
<string name="action_edit_list">Ändra</string> <string name="action_edit_list">Redigera lista</string>
<string name="hint_search_people_list">Sök efter personer du följer</string> <string name="hint_search_people_list">Sök efter personer du följer</string>
<string name="action_add_to_list">Lägg till konto i listan</string> <string name="action_add_to_list">Lägg till konto i listan</string>
<string name="action_remove_from_list">Ta bort kontot från listan</string> <string name="action_remove_from_list">Ta bort kontot från listan</string>
@ -287,7 +287,7 @@
<string name="lock_account_label_description">Kräver att du manuellt godkänner följare</string> <string name="lock_account_label_description">Kräver att du manuellt godkänner följare</string>
<string name="compose_save_draft">Spara utkast?</string> <string name="compose_save_draft">Spara utkast?</string>
<string name="send_toot_notification_title">Skickar toot…</string> <string name="send_toot_notification_title">Skickar toot…</string>
<string name="send_toot_notification_error_title">Fel vid sändning av toot</string> <string name="send_toot_notification_error_title">Kunde inte skicka toot</string>
<string name="send_toot_notification_channel_name">Skickar toot</string> <string name="send_toot_notification_channel_name">Skickar toot</string>
<string name="send_toot_notification_cancel_title">Sändning avbruten</string> <string name="send_toot_notification_cancel_title">Sändning avbruten</string>
<string name="send_toot_notification_saved_content">En kopia av tooten har sparats i dina utkast</string> <string name="send_toot_notification_saved_content">En kopia av tooten har sparats i dina utkast</string>
@ -298,14 +298,14 @@
<string name="system_default">Systemstandard</string> <string name="system_default">Systemstandard</string>
<string name="download_fonts">Du behöver ladda ned dessa emojis först</string> <string name="download_fonts">Du behöver ladda ned dessa emojis först</string>
<string name="performing_lookup_title">Utför sökning…</string> <string name="performing_lookup_title">Utför sökning…</string>
<string name="expand_collapse_all_statuses">Expandera/Dölj alla status</string> <string name="expand_collapse_all_statuses">Expandera/Dölj alla statusar</string>
<string name="action_open_toot">Öppna toot</string> <string name="action_open_toot">Öppna toot</string>
<string name="restart_required">Omstart av appen krävs</string> <string name="restart_required">Omstart av appen krävs</string>
<string name="restart_emoji">Du måste starta om Yuito för att tillämpa ändringarna</string> <string name="restart_emoji">Du måste starta om Yuito för att tillämpa ändringarna</string>
<string name="later">Senare</string> <string name="later">Senare</string>
<string name="restart">Starta om</string> <string name="restart">Starta om</string>
<string name="caption_systememoji">Standard-emojis för din enhet</string> <string name="caption_systememoji">Standard-emojis för din enhet</string>
<string name="caption_blobmoji">Emojis baserade på The Blob emojis kända från Android 4.47.1</string> <string name="caption_blobmoji">Emojis baserade på The Blob emojis från Android 4.47.1</string>
<string name="caption_twemoji">Mastodon\'s standard emojis</string> <string name="caption_twemoji">Mastodon\'s standard emojis</string>
<string name="download_failed">Nedladdning misslyckad</string> <string name="download_failed">Nedladdning misslyckad</string>
<string name="profile_badge_bot_text">Robot</string> <string name="profile_badge_bot_text">Robot</string>
@ -373,9 +373,9 @@
<string name="pref_title_bot_overlay">Visa robotindikator</string> <string name="pref_title_bot_overlay">Visa robotindikator</string>
<string name="notification_clear_text">Är du säker på att du vill rensa dina notifieringar permanent\?</string> <string name="notification_clear_text">Är du säker på att du vill rensa dina aviseringar permanent\?</string>
<string name="action_delete_and_redraft">Radera och skriv nytt</string> <string name="action_delete_and_redraft">Radera och skriv nytt</string>
<string name="dialog_redraft_toot_warning">Radera och skriv ny toot\?</string> <string name="dialog_redraft_toot_warning">Radera och skriv ny toot\?</string>
<string name="poll_info_format"> <!-- 15 röster • 1 timme kvar --> %1$s • %2$s</string> <string name="poll_info_format"> <!-- 15 röster • 1 timme kvar --> %1$s • %2$s</string>
@ -384,15 +384,15 @@
<item quantity="other">%s röster</item> <item quantity="other">%s röster</item>
</plurals> </plurals>
<string name="poll_info_time_relative">%s kvar</string> <string name="poll_info_time_relative">%s kvar</string>
<string name="poll_info_time_absolute">avslutas %s</string> <string name="poll_info_time_absolute">avslutas vid %s</string>
<string name="poll_info_closed">stängd</string> <string name="poll_info_closed">stängd</string>
<string name="poll_vote">Rösta</string> <string name="poll_vote">Rösta</string>
<string name="pref_title_notification_filter_poll">omröstning är avslutad</string> <string name="pref_title_notification_filter_poll">omröstningar har avslutats</string>
<string name="notification_poll_name">Omröstningar</string> <string name="notification_poll_name">Omröstningar</string>
<string name="notification_poll_description">Notifieringar när omröstningar har avslutats</string> <string name="notification_poll_description">Aviseringar när omröstningar har avslutats</string>
<string name="poll_ended_voted">En omröstning där du har röstat är avslutad</string> <string name="poll_ended_voted">En omröstning där du har röstat är avslutad</string>
@ -424,9 +424,9 @@
<string name="title_domain_mutes">Dolda domäner</string> <string name="title_domain_mutes">Dolda domäner</string>
<string name="action_view_domain_mutes">Dolda domäner</string> <string name="action_view_domain_mutes">Dolda domäner</string>
<string name="action_mute_domain">Tysta %s</string> <string name="action_mute_domain">Tysta %s</string>
<string name="confirmation_domain_unmuted">%s inte tystnad längre</string> <string name="confirmation_domain_unmuted">%s inte tystad längre</string>
<string name="mute_domain_warning">Är du säker på att du vill blockera allt från %s\? Du kommer inte kunna se något innehåll från denna domän i publika tidslinje eller i dina notifieringar. Dina följare på domänen kommer inte att bli borttagna.</string> <string name="mute_domain_warning">Är du säker på att du vill blockera allt från %s\? Du kommer inte kunna se något innehåll från denna domän i publika tidslinjer eller i dina notifieringar. Dina följare på domänen kommer inte att bli borttagna.</string>
<string name="mute_domain_warning_dialog_ok">Dölj hela domänen</string> <string name="mute_domain_warning_dialog_ok">Dölj hela domänen</string>
<string name="caption_notoemoji">Google\'s nuvarande emojis</string> <string name="caption_notoemoji">Google\'s nuvarande emojis</string>
@ -443,7 +443,7 @@
<string name="pref_title_show_notifications_filter">Visa notifikationsfilter</string> <string name="pref_title_show_notifications_filter">Visa notifikationsfilter</string>
<string name="filter_dialog_whole_word">Helt ord</string> <string name="filter_dialog_whole_word">Helt ord</string>
<string name="filter_dialog_whole_word_description">När nyckelordet eller frasen är alfanumerisk enbart, blir den enbart appliceras om den matchar hela ordet</string> <string name="filter_dialog_whole_word_description">När nyckelordet eller frasen enbart är alfanumerisk, appliceras den om den matchar hela ordet</string>
<string name="pref_title_alway_open_spoiler">Expandera alltid toots med innehållsvarningar</string> <string name="pref_title_alway_open_spoiler">Expandera alltid toots med innehållsvarningar</string>
<string name="title_accounts">Konton</string> <string name="title_accounts">Konton</string>
<string name="failed_search">Sökning misslyckades</string> <string name="failed_search">Sökning misslyckades</string>
@ -482,4 +482,5 @@
<string name="error_audio_upload_size">Ljudfiler måste vara mindre än 40MB.</string> <string name="error_audio_upload_size">Ljudfiler måste vara mindre än 40MB.</string>
<string name="no_saved_status">Du har inga utkast.</string> <string name="no_saved_status">Du har inga utkast.</string>
<string name="warning_scheduling_interval">Mastodon har ett minimalt schemaläggningsintervall på 5 minuter.</string>
</resources> </resources>

View File

@ -161,7 +161,7 @@
<string name="login_connection">正在連線…</string> <string name="login_connection">正在連線…</string>
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>

View File

@ -161,7 +161,7 @@
<string name="login_connection">正在連線…</string> <string name="login_connection">正在連線…</string>
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>

View File

@ -161,7 +161,7 @@
<string name="login_connection">正在連線…</string> <string name="login_connection">正在連線…</string>
<string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> <string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</string>
<string name="dialog_title_finishing_media_upload">正在完成上傳…</string> <string name="dialog_title_finishing_media_upload">正在完成上傳…</string>

View File

@ -22,4 +22,5 @@
<item name="action_open_reblogger" type="id" /> <item name="action_open_reblogger" type="id" />
<item name="action_open_reblogged_by" type="id" /> <item name="action_open_reblogged_by" type="id" />
<item name="action_open_faved_by" type="id" /> <item name="action_open_faved_by" type="id" />
<item name="action_more" type="id" />
</resources> </resources>

View File

@ -7,7 +7,6 @@
<dimen name="compose_media_preview_margin">8dp</dimen> <dimen name="compose_media_preview_margin">8dp</dimen>
<dimen name="compose_media_preview_margin_bottom">0dp</dimen> <dimen name="compose_media_preview_margin_bottom">0dp</dimen>
<dimen name="compose_media_preview_size">120dp</dimen> <dimen name="compose_media_preview_size">120dp</dimen>
<dimen name="compose_options_margin">8dp</dimen>
<dimen name="account_avatar_margin">14dp</dimen> <dimen name="account_avatar_margin">14dp</dimen>
<dimen name="tab_page_margin">16dp</dimen> <dimen name="tab_page_margin">16dp</dimen>
<dimen name="status_line_margin_start">36dp</dimen> <dimen name="status_line_margin_start">36dp</dimen>

View File

@ -65,6 +65,7 @@
<string name="notification_reblog_format">%s boosted your toot</string> <string name="notification_reblog_format">%s boosted your toot</string>
<string name="notification_favourite_format">%s favorited your toot</string> <string name="notification_favourite_format">%s favorited your toot</string>
<string name="notification_follow_format">%s followed you</string> <string name="notification_follow_format">%s followed you</string>
<string name="notification_follow_request_format">%s requested to follow you</string>
<string name="report_username_format">Report @%s</string> <string name="report_username_format">Report @%s</string>
<string name="report_comment_hint">Additional comments?</string> <string name="report_comment_hint">Additional comments?</string>
@ -113,6 +114,8 @@
<string name="action_mute">Mute</string> <string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</string> <string name="action_unmute">Unmute</string>
<string name="action_mute_domain">Mute %s</string> <string name="action_mute_domain">Mute %s</string>
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="action_mention">Mention</string> <string name="action_mention">Mention</string>
<string name="action_hide_media">Hide media</string> <string name="action_hide_media">Hide media</string>
<string name="action_open_drawer">Open drawer</string> <string name="action_open_drawer">Open drawer</string>
@ -205,6 +208,8 @@
<string name="dialog_redraft_toot_warning">Delete and re-draft this toot?</string> <string name="dialog_redraft_toot_warning">Delete and re-draft this toot?</string>
<string name="mute_domain_warning">Are you sure you want to block all of %s? You will not see content from that domain in any public timelines or in your notifications. Your followers from that domain will be removed.</string> <string name="mute_domain_warning">Are you sure you want to block all of %s? You will not see content from that domain in any public timelines or in your notifications. Your followers from that domain will be removed.</string>
<string name="mute_domain_warning_dialog_ok">Hide entire domain</string> <string name="mute_domain_warning_dialog_ok">Hide entire domain</string>
<string name="dialog_block_warning">Block @%s?</string>
<string name="dialog_mute_warning">Mute @%s?</string>
<string name="visibility_public">Public: Post to public timelines</string> <string name="visibility_public">Public: Post to public timelines</string>
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string> <string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>
@ -221,6 +226,7 @@
<string name="pref_title_notification_filters">Notify me when</string> <string name="pref_title_notification_filters">Notify me when</string>
<string name="pref_title_notification_filter_mentions">mentioned</string> <string name="pref_title_notification_filter_mentions">mentioned</string>
<string name="pref_title_notification_filter_follows">followed</string> <string name="pref_title_notification_filter_follows">followed</string>
<string name="pref_title_notification_filter_follow_requests">follow requested</string>
<string name="pref_title_notification_filter_reblogs">my posts are boosted</string> <string name="pref_title_notification_filter_reblogs">my posts are boosted</string>
<string name="pref_title_notification_filter_favourites">my posts are favorited</string> <string name="pref_title_notification_filter_favourites">my posts are favorited</string>
<string name="pref_title_notification_filter_poll">polls have ended</string> <string name="pref_title_notification_filter_poll">polls have ended</string>
@ -284,6 +290,8 @@
<string name="notification_mention_descriptions">Notifications about new mentions</string> <string name="notification_mention_descriptions">Notifications about new mentions</string>
<string name="notification_follow_name">New Followers</string> <string name="notification_follow_name">New Followers</string>
<string name="notification_follow_description">Notifications about new followers</string> <string name="notification_follow_description">Notifications about new followers</string>
<string name="notification_follow_request_name">Follow Requests</string>
<string name="notification_follow_request_description">Notifications about follow requests</string>
<string name="notification_boost_name">Boosts</string> <string name="notification_boost_name">Boosts</string>
<string name="notification_boost_description">Notifications when your toots get boosted</string> <string name="notification_boost_description">Notifications when your toots get boosted</string>
<string name="notification_favourite_name">Favorites</string> <string name="notification_favourite_name">Favorites</string>
@ -516,6 +524,10 @@
<item quantity="one">%s vote</item> <item quantity="one">%s vote</item>
<item quantity="other">%s votes</item> <item quantity="other">%s votes</item>
</plurals> </plurals>
<plurals name="poll_info_people">
<item quantity="one">%s person</item>
<item quantity="other">%s people</item>
</plurals>
<string name="poll_info_time_relative">%s left</string> <string name="poll_info_time_relative">%s left</string>
<string name="poll_info_time_absolute">ends at %s</string> <string name="poll_info_time_absolute">ends at %s</string>
<string name="poll_info_closed">closed</string> <string name="poll_info_closed">closed</string>
@ -576,5 +588,7 @@
<string name="no_saved_status">You don\'t have any drafts.</string> <string name="no_saved_status">You don\'t have any drafts.</string>
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string> <string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string> <string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
<string name="pref_title_show_cards_in_timelines">Show link previews in timelines</string>
<string name="pref_title_confirm_reblogs">Show confirmation dialog before boosting</string>
</resources> </resources>

View File

@ -27,6 +27,12 @@
android:title="@string/pref_title_notification_filter_follows" android:title="@string/pref_title_notification_filter_follows"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="notificationFilterFollowRequests"
android:title="@string/pref_title_notification_filter_follow_requests"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="notificationFilterReblogs" android:key="notificationFilterReblogs"

View File

@ -72,6 +72,18 @@
android:title="@string/pref_title_show_notifications_filter" android:title="@string/pref_title_show_notifications_filter"
app:singleLineTitle="false" /> app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="showCardsInTimelines"
android:title="@string/pref_title_show_cards_in_timelines"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="confirmReblogs"
android:title="@string/pref_title_confirm_reblogs"
app:singleLineTitle="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/pref_title_limited_bandwidth_settings"> <PreferenceCategory android:title="@string/pref_title_limited_bandwidth_settings">

View File

@ -88,6 +88,7 @@ class BottomSheetActivityTest {
arrayOf(), arrayOf(),
null, null,
pinned = false, pinned = false,
muted = false,
poll = null, poll = null,
card = null card = null
) )

View File

@ -68,6 +68,7 @@ class ComposeActivityTest {
notificationsEnabled = true, notificationsEnabled = true,
notificationsMentioned = true, notificationsMentioned = true,
notificationsFollowed = true, notificationsFollowed = true,
notificationsFollowRequested = false,
notificationsReblogged = true, notificationsReblogged = true,
notificationsFavorited = true, notificationsFavorited = true,
notificationSound = true, notificationSound = true,

View File

@ -11,6 +11,7 @@ import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import okhttp3.Request import okhttp3.Request
import okio.Timeout
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
@ -99,6 +100,10 @@ class FilterTest {
) )
) )
} }
override fun timeout(): Timeout {
throw Error("not implemented")
}
}) })
activity.mastodonApi = apiMock activity.mastodonApi = apiMock
@ -214,6 +219,7 @@ class FilterTest {
mentions = emptyArray(), mentions = emptyArray(),
application = null, application = null,
pinned = false, pinned = false,
muted = false,
poll = if (pollOptions != null) { poll = if (pollOptions != null) {
Poll( Poll(
id = "1234", id = "1234",
@ -221,6 +227,7 @@ class FilterTest {
expired = false, expired = false,
multiple = false, multiple = false,
votesCount = 0, votesCount = 0,
votersCount = 0,
options = pollOptions.map { options = pollOptions.map {
PollOption(it, 0) PollOption(it, 0)
}, },

View File

@ -18,13 +18,9 @@ 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.util.Log
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.LocaleManager
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat.FileEmojiCompatConfig import de.c1710.filemojicompat.FileEmojiCompatConfig
import javax.inject.Inject
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff // override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() { class TuskyApplication : Application() {

View File

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.fragment package com.keylesspalace.tusky.fragment
import android.text.Spanned import android.text.SpannableString
import android.text.SpannedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.gson.Gson import com.google.gson.Gson
import com.keylesspalace.tusky.SpanUtilsTest import com.keylesspalace.tusky.SpanUtilsTest
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
@ -12,7 +14,6 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.* import com.keylesspalace.tusky.repository.*
import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HtmlConverter
import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.isNull
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
@ -24,14 +25,18 @@ import io.reactivex.schedulers.TestScheduler
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class TimelineRepositoryTest { class TimelineRepositoryTest {
@Mock @Mock
lateinit var timelineDao: TimelineDao lateinit var timelineDao: TimelineDao
@ -56,15 +61,6 @@ class TimelineRepositoryTest {
domain = "domain.com", domain = "domain.com",
isActive = true isActive = true
) )
private val htmlConverter = object : HtmlConverter {
override fun fromHtml(html: String): Spanned {
return SpanUtilsTest.FakeSpannable(html)
}
override fun toHtml(text: Spanned): String {
return text.toString()
}
}
@Before @Before
fun setup() { fun setup() {
@ -74,8 +70,7 @@ class TimelineRepositoryTest {
gson = Gson() gson = Gson()
testScheduler = TestScheduler() testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler } RxJavaPlugins.setIoSchedulerHandler { testScheduler }
subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson, subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson)
htmlConverter)
} }
@Test @Test
@ -97,7 +92,7 @@ class TimelineRepositoryTest {
verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id)) verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id))
for (status in statuses) { for (status in statuses) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -129,7 +124,7 @@ class TimelineRepositoryTest {
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -159,7 +154,7 @@ class TimelineRepositoryTest {
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id) verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -201,7 +196,7 @@ class TimelineRepositoryTest {
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -246,7 +241,7 @@ class TimelineRepositoryTest {
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, gson),
status.account.toEntity(account.id, gson), status.account.toEntity(account.id, gson),
null null
) )
@ -263,7 +258,7 @@ class TimelineRepositoryTest {
val status = makeStatus("2") val status = makeStatus("2")
val dbStatus = makeStatus("1") val dbStatus = makeStatus("1")
val dbResult = TimelineStatusWithAccount() val dbResult = TimelineStatusWithAccount()
dbResult.status = dbStatus.toEntity(account.id, htmlConverter, gson) dbResult.status = dbStatus.toEntity(account.id, gson)
dbResult.account = status.account.toEntity(account.id, gson) dbResult.account = status.account.toEntity(account.id, gson)
whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
@ -297,7 +292,7 @@ class TimelineRepositoryTest {
return Status( return Status(
id = id, id = id,
account = account, account = account,
content = SpanUtilsTest.FakeSpannable("hello$id"), content = SpannableString("hello$id"),
createdAt = Date(), createdAt = Date(),
emojis = listOf(), emojis = listOf(),
reblogsCount = 3, reblogsCount = 3,
@ -314,6 +309,7 @@ class TimelineRepositoryTest {
inReplyToAccountId = null, inReplyToAccountId = null,
inReplyToId = null, inReplyToId = null,
pinned = false, pinned = false,
muted = false,
reblog = null, reblog = null,
url = "http://example.com/statuses/$id", url = "http://example.com/statuses/$id",
poll = null, poll = null,
@ -328,7 +324,7 @@ class TimelineRepositoryTest {
localUsername = "test$id", localUsername = "test$id",
username = "test$id@example.com", username = "test$id@example.com",
displayName = "Example Account $id", displayName = "Example Account $id",
note = SpanUtilsTest.FakeSpannable("Note! $id"), note = SpannableString("Note! $id"),
url = "https://example.com/@test$id", url = "https://example.com/@test$id",
avatar = "avatar$id", avatar = "avatar$id",
header = "Header$id", header = "Header$id",

View File

@ -1,11 +1,11 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.61' ext.kotlin_version = '1.3.71'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.6.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -1 +1 @@
Un client per a diversos comptes per a la xarxa social Mastodont Un client multicomptes per a la xarxa social Mastodont

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