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.roomVersion = '2.2.4'
ext.retrofitVersion = '2.7.1'
ext.okhttpVersion = '4.3.1'
ext.glideVersion = '4.10.0'
ext.daggerVersion = '2.26'
ext.roomVersion = '2.2.5'
ext.retrofitVersion = '2.8.1'
ext.okhttpVersion = '4.4.0'
ext.glideVersion = '4.11.0'
ext.daggerVersion = '2.27'
repositories {
maven {
@ -120,12 +120,12 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.appcompat:appcompat:1.2.0-alpha02"
implementation "androidx.fragment:fragment-ktx:1.2.2"
implementation "androidx.appcompat:appcompat:1.2.0-beta01"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.browser:browser:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.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.preference:preference:1.1.0"
implementation "androidx.sharetarget:sharetarget:1.0.0-rc01"
@ -135,7 +135,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
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.room:room-runtime:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomVersion"
@ -150,7 +150,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$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:okhttp3-integration:$glideVersion"
@ -168,7 +168,7 @@ dependencies {
implementation "com.google.dagger:dagger-android-support:$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"

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.di.ViewModelFactory
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.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener
@ -311,7 +309,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Subscribe to data loaded at the view model
*/
private fun subscribeObservables() {
viewModel.accountData.observe(this, Observer<Resource<Account>> {
viewModel.accountData.observe(this, Observer {
when (it) {
is Success -> onAccountChanged(it.data)
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
if (relation != null) {
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.notifyDataSetChanged()
@ -681,6 +679,30 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.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() {
loadedAccount?.let {
val intent = ComposeActivity.startIntent(this,
@ -727,11 +749,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return true
}
R.id.action_block -> {
viewModel.changeBlockState()
toggleBlock()
return true
}
R.id.action_mute -> {
viewModel.changeMuteState()
toggleMute()
return true
}
R.id.action_mute_domain -> {

View File

@ -77,7 +77,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
val args = arguments!!
val args = requireArguments()
listId = args.getString(LIST_ID_ARG)!!
listName = args.getString(LIST_NAME_ARG)!!
@ -255,12 +255,12 @@ class AccountsInListFragment : DialogFragment(), Injectable {
loadAvatar(account.avatar, avatar, radius, animateAvatar)
rejectButton.apply {
if (inAList) {
contentDescription = if (inAList) {
setImageResource(R.drawable.ic_reject_24dp)
contentDescription = getString(R.string.action_remove_from_list)
getString(R.string.action_remove_from_list)
} else {
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)
editText.onTextChanged { s, _, _, _ ->
positiveButton.isEnabled = !s.isNullOrBlank()
positiveButton.isEnabled = !s.isBlank()
}
editText.setText(list?.title)
editText.text?.let { editText.setSelection(it.length) }

View File

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

View File

@ -277,7 +277,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
addTabAdapter.updateData(addableTabs)
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) {

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.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
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.ImageLoadingHelper;
public class FollowRequestsAdapter extends AccountAdapter {
@ -46,7 +39,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view);
return new FollowRequestViewHolder(view, false);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
@ -60,57 +53,8 @@ public class FollowRequestsAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupWithAccount(accountList.get(position), null);
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.Notification;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
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_NOTIFICATION = 1;
private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_PLACEHOLDER = 3;
private static final int VIEW_TYPE_UNKNOWN = 4;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
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[] NO_INPUT_FILTER = new InputFilter[0];
@ -85,6 +88,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource;
@ -92,13 +96,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
AdapterDataSource<NotificationViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener,
NotificationActionListener notificationActionListener) {
NotificationActionListener notificationActionListener,
AccountActionListener accountActionListener) {
this.accountId = accountId;
this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener;
this.accountActionListener = accountActionListener;
bidiFormatter = BidiFormatter.getInstance();
}
@ -122,6 +128,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_follow, parent, false);
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: {
View view = inflater
.inflate(R.layout.item_status_placeholder, parent, false);
@ -218,6 +229,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
break;
}
case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
holder.setupActionListener(accountActionListener);
}
}
default:
}
}
@ -234,7 +252,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash()
statusDisplayOptions.useBlurhash(),
CardViewMode.NONE,
statusDisplayOptions.confirmReblogs()
);
}
@ -259,6 +279,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
}
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
default: {
return VIEW_TYPE_UNKNOWN;
}

View File

@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.HtmlUtils
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.PollOptionViewData
import com.keylesspalace.tusky.viewdata.buildDescription
@ -36,12 +35,19 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
private var pollOptions: List<PollOptionViewData> = emptyList()
private var voteCount: Int = 0
private var votersCount: Int? = null
private var mode = RESULT
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.voteCount = voteCount
this.votersCount = votersCount
this.emojis = emojis
this.mode = mode
notifyDataSetChanged()
@ -71,7 +77,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
when(mode) {
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)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)

View File

@ -8,31 +8,38 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
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.google.android.material.button.MaterialButton;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -92,6 +99,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private TextView pollDescription;
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 SimpleDateFormat shortSdf;
@ -152,6 +165,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollDescription = itemView.findViewById(R.id.status_poll_description);
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();
pollOptions.setAdapter(pollAdapter);
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
@ -200,7 +220,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
contentWarningDescription.setVisibility(View.VISIBLE);
contentWarningButton.setVisibility(View.VISIBLE);
setContentWarningButtonText(expanded);
contentWarningButton.setOnClickListener( view -> {
contentWarningButton.setOnClickListener(view -> {
contentWarningDescription.invalidate();
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(!expanded, getAdapterPosition());
@ -218,7 +238,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
private void setContentWarningButtonText(boolean expanded) {
if(expanded) {
if (expanded) {
contentWarningButton.setText(R.string.status_content_warning_show_less);
} else {
contentWarningButton.setText(R.string.status_content_warning_show_more);
@ -673,8 +693,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setVisibility(View.GONE);
}
protected void setupButtons(final StatusActionListener listener, final String accountId,
final boolean isNotestock, final String acct) {
protected void setupButtons(final StatusActionListener listener,
final String accountId,
final String statusContent,
final boolean isNotestock,
final String acct,
StatusDisplayOptions statusDisplayOptions) {
avatar.setOnClickListener(v -> {
if (isNotestock) {
@ -702,9 +726,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
replyButton.setClickable(!isNotestock);
if (reblogButton != null) {
reblogButton.setEventListener((button, buttonState) -> {
// return true to play animaion
int position = getAdapterPosition();
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) -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onFavourite(buttonState, position);
listener.onFavourite(!buttonState, position);
}
return true;
});
favouriteButton.setEnabled(!isNotestock);
@ -731,8 +765,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton.setEventListener((button, buttonState) -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onBookmark(buttonState, position);
listener.onBookmark(!buttonState, position);
}
return true;
});
moreButton.setOnClickListener(v -> {
@ -757,6 +792,23 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
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,
StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, statusDisplayOptions, null);
@ -799,8 +851,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
hideSensitiveMediaWarning();
}
setupButtons(listener, status.getSenderId(), status.isNotestock(), status.getNickname());
setRebloggingEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
if (cardView != null) {
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());
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();
for (int i = 0; i < args.length; i++) {
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);
} else {
args[i] = "";
@ -949,7 +1006,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getFavsText(Context context, int count) {
if (count > 0) {
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 {
return "";
}
@ -958,7 +1015,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected CharSequence getReblogsText(Context context, int count) {
if (count > 0) {
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 {
return "";
}
@ -977,12 +1034,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (expired || poll.getVoted()) {
// 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);
} else {
// 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);
@ -1009,8 +1066,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private CharSequence getPollInfoText(long timestamp, PollViewData poll,
StatusDisplayOptions statusDisplayOptions,
Context context) {
String votes = numberFormat.format(poll.getVotesCount());
String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
String votesText;
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;
if (poll.getExpired()) {
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);
}
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) {
int seconds = (int) Math.round(durationInSeconds) % 60;
int minutes = (int) durationInSeconds % 3600 / 60;

View File

@ -5,26 +5,19 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
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.ViewThreadActivity;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -37,27 +30,13 @@ import java.util.regex.Pattern;
class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView reblogs;
private TextView favourites;
private LinearLayout cardView;
private LinearLayout cardInfo;
private ImageView cardImage;
private TextView cardTitle;
private TextView cardDescription;
private TextView cardUrl;
private View infoDivider;
StatusDetailedViewHolder(View view) {
super(view);
reblogs = view.findViewById(R.id.status_reblogs);
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);
cardView.setClipToOutline(true);
}
@Override
@ -131,6 +110,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status
if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
@ -149,97 +129,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
content.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,
statusDisplayOptions.useAbsoluteTime(),
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 ReblogEvent(val statusId: String, val reblog: 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 BlockEvent(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) {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
val shareBody = if (subject != null && text != null) {
if (subject != text && !text.contains(subject)) {
String.format("%s\n%s", subject, text)
} else {
text
}
} else text ?: subject
val shareBody = text ?: subject
if (shareBody != null) {
if (!subject.isNullOrBlank() && subject !in shareBody) {
composeContentWarningField.setText(subject)
viewModel.showContentWarning.value = true
}
val start = composeEditField.selectionStart.coerceAtLeast(0)
val end = composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(start, end)

View File

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

View File

@ -18,26 +18,19 @@ package com.keylesspalace.tusky.components.compose.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.RadioGroup
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Status
import kotlinx.android.synthetic.main.view_compose_options.view.*
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) {
var listener: ComposeOptionsListener? = null
init {
inflate(context, R.layout.view_compose_options, this)
publicRadioButton.setButtonDrawable(R.drawable.ic_public_24dp)
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 ->
setOnCheckedChangeListener { _, checkedId ->
val visibility = when (checkedId) {
R.id.publicRadioButton ->
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) {
initializeSuggestedTime();
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);
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
openPickTimeDialog();

View File

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

View File

@ -104,7 +104,8 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
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(),
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.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_timeline.*
import javax.inject.Inject
@ -68,7 +65,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
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.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject
@ -119,7 +116,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true)
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", false)
)
adapter = StatusesAdapter(statusDisplayOptions,

View File

@ -243,7 +243,7 @@ class SearchViewModel @Inject constructor(
return accountManager.getAllAccountsOrderedByActive()
}
fun muteAcount(accountId: String) {
fun muteAccount(accountId: String) {
timelineCases.mute(accountId)
}
@ -263,6 +263,18 @@ class SearchViewModel @Inject constructor(
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 {
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.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.AttachmentViewData
@ -70,7 +72,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
get() = viewModel.statuses
private val searchAdapter
get() = super.adapter as SearchStatusesAdapter
get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
@ -79,7 +81,9 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
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))
@ -250,12 +254,9 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
val loggedInAccountId = viewModel.activeAccount?.accountId
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.
if (loggedInAccountId == null || loggedInAccountId != accountId) {
popup.inflate(R.menu.status_more)
val menu = popup.menu
menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty()
} else {
if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more_for_user)
val menu = popup.menu
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 -> {
} //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)
@ -288,6 +293,19 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
}
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 ->
when (item.itemId) {
R.id.status_share_content -> {
@ -325,12 +343,18 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
requestDownloadAllMedia(status)
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 -> {
viewModel.muteAcount(accountId)
onMute(accountId, accountUsername)
return@setOnMenuItemClickListener true
}
R.id.status_block -> {
viewModel.blockAccount(accountId)
onBlock(accountId, accountUsername)
return@setOnMenuItemClickListener true
}
R.id.status_report -> {
@ -363,6 +387,28 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
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) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
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 notificationsMentioned: Boolean = true,
var notificationsFollowed: Boolean = true,
var notificationsFollowRequested: Boolean = false,
var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true,
@ -54,7 +55,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var activeNotifications: String = "[]",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[]") {
var notificationsFilter: String = "[\"follow_request\"]") {
val identifier: String
get() = "$domain:$accountId"

View File

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 21)
}, version = 23)
public abstract class AppDatabase extends RoomDatabase {
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
import android.text.Spanned
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import androidx.room.TypeConverter
import com.google.gson.GsonBuilder
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.Status
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.HtmlUtils
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.*
@ -128,7 +129,7 @@ class Converters {
if(spanned == null) {
return null
}
return HtmlUtils.toHtml(spanned)
return spanned.toHtml()
}
@TypeConverter
@ -136,7 +137,7 @@ class Converters {
if(spannedString == null) {
return null
}
return HtmlUtils.fromHtml(spannedString)
return spannedString.parseAsHtml()
}
@TypeConverter

View File

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

View File

@ -29,8 +29,6 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.network.TimelineCasesImpl
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.HtmlConverterImpl
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@ -79,13 +77,9 @@ class AppModule {
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_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
.build()
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
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.repository.TimelineRepository
import com.keylesspalace.tusky.repository.TimelineRepositoryImpl
import com.keylesspalace.tusky.util.HtmlConverter
import dagger.Module
import dagger.Provides
@Module
class RepositoryModule {
@Provides
fun providesTimelineRepository(db: AppDatabase, mastodonApi: MastodonApi,
accountManager: AccountManager, gson: Gson,
htmlConverter: HtmlConverter): TimelineRepository {
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson,
htmlConverter)
fun providesTimelineRepository(
db: AppDatabase,
mastodonApi: MastodonApi,
accountManager: AccountManager,
gson: Gson
): TimelineRepository {
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson)
}
}

View File

@ -15,24 +15,16 @@
package com.keylesspalace.tusky.entity
import android.os.Parcel
import android.os.Parcelable
import android.text.Spanned
import com.google.gson.annotations.SerializedName
import com.keylesspalace.tusky.util.HtmlUtils
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
import java.util.*
import java.util.Date
@Parcelize
data class Account(
val id: String,
@SerializedName("username") val localUsername: 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
val note: @WriteWith<SpannedParceler>() Spanned,
val note: Spanned,
val url: String,
val avatar: String,
val header: String,
@ -47,7 +39,7 @@ data class Account(
val moved: Account? = null,
@SerializedName("name") val notestockUsername: String? = null
) : Parcelable {
) {
val name: String
get() = notestockUsername ?: if (displayName.isNullOrEmpty()) {
@ -87,31 +79,20 @@ data class Account(
fun isRemote(): Boolean = this.username != this.localUsername
}
@Parcelize
data class AccountSource(
val privacy: Status.Visibility,
val sensitive: Boolean,
val note: String,
val fields: List<StringField>?
): Parcelable
)
@Parcelize
data class Field (
val name: String,
val value: @WriteWith<SpannedParceler>() Spanned,
val value: Spanned,
@SerializedName("verified_at") val verifiedAt: Date?
): Parcelable
)
@Parcelize
data class StringField (
val name: 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
import android.os.Parcelable
import android.text.Spanned
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.WriteWith
@Parcelize
data class Card(
val url: String,
val title: @WriteWith<SpannedParceler>() Spanned,
val description: @WriteWith<SpannedParceler>() Spanned,
val title: Spanned,
val description: Spanned,
@SerializedName("author_name") val authorName: String,
val image: String,
val type: String,
val width: Int,
val height: Int
) : Parcelable {
) {
override fun hashCode(): Int {
return url.hashCode()

View File

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

View File

@ -9,6 +9,7 @@ data class Poll(
val expired: Boolean,
val multiple: Boolean,
@SerializedName("votes_count") val votesCount: Int,
@SerializedName("voters_count") val votersCount: Int?, // nullable for compatibility with Pleroma
val options: List<PollOption>,
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(

View File

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

View File

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

View File

@ -65,10 +65,13 @@ import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -97,6 +100,7 @@ import javax.inject.Inject;
import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
@ -114,6 +118,7 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
NotificationsAdapter.NotificationActionListener,
AccountActionListener,
Injectable, ReselectableFragment {
private static final String TAG = "NotificationF"; // logging tag
@ -244,11 +249,13 @@ public class NotificationsFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true)
);
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, statusDisplayOptions, this, this);
dataSource, statusDisplayOptions, this, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
recyclerView.setAdapter(adapter);
@ -767,6 +774,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_boost_name);
case FOLLOW:
return getString(R.string.notification_follow_name);
case FOLLOW_REQUEST:
return getString(R.string.notification_follow_request_name);
case POLL:
return getString(R.string.notification_poll_name);
default:
@ -819,6 +828,29 @@ public class NotificationsFragment extends SFragment implements
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
public void onViewStatusForNotificationId(String notificationId) {
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.view.ViewCompat;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity;
@ -215,11 +216,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
PopupMenu popup = new PopupMenu(getContext(), view);
// Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) {
popup.inflate(R.menu.status_more);
Menu menu = popup.getMenu();
menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty());
} else {
boolean statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId.equals(accountId);
if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more_for_user);
Menu menu = popup.getMenu();
switch (status.getVisibility()) {
@ -238,6 +236,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
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();
@ -261,6 +263,15 @@ public abstract class SFragment extends BaseFragment implements Injectable {
}
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 -> {
switch (item.getItemId()) {
case R.id.status_share_content: {
@ -304,11 +315,11 @@ public abstract class SFragment extends BaseFragment implements Injectable {
return true;
}
case R.id.status_mute: {
timelineCases.mute(accountId);
onMute(accountId, accountUsername);
return true;
}
case R.id.status_block: {
timelineCases.block(accountId);
onBlock(accountId, accountUsername);
return true;
}
case R.id.status_report: {
@ -335,12 +346,52 @@ public abstract class SFragment extends BaseFragment implements Injectable {
timelineCases.pin(status, !status.isPinned());
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;
});
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) {
final Status actionable = status.getActionableStatus();
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.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent;
import com.keylesspalace.tusky.appstore.MuteConversationEvent;
import com.keylesspalace.tusky.appstore.MuteEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
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.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
@ -229,7 +231,7 @@ public class TimelineFragment extends SFragment implements
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = Objects.requireNonNull(getArguments());
Bundle arguments = requireArguments();
kind = Kind.valueOf(arguments.getString(KIND_ARG));
if (kind == Kind.TAG
|| kind == Kind.USER
@ -245,7 +247,11 @@ public class TimelineFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
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);
@ -568,6 +574,9 @@ public class TimelineFragment extends SFragment implements
} else if (event instanceof BookmarkEvent) {
BookmarkEvent bookmarkEvent = (BookmarkEvent) event;
handleBookmarkEvent(bookmarkEvent);
} else if (event instanceof MuteConversationEvent) {
MuteConversationEvent muteEvent = (MuteConversationEvent) event;
handleMuteConversationEvent(muteEvent);
} else if (event instanceof UnfollowEvent) {
if (kind == Kind.HOME) {
String id = ((UnfollowEvent) event).getAccountId();
@ -1428,6 +1437,10 @@ public class TimelineFragment extends SFragment implements
setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark());
}
private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) {
fullyRefresh();
}
private void handleStatusComposeEvent(@NonNull Status status) {
switch (kind) {
case HOME:

View File

@ -89,7 +89,7 @@ class ViewImageFragment : ViewMediaFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = activity!!.toolbar
toolbar = requireActivity().toolbar
this.transition = BehaviorSubject.create()
return inflater.inflate(R.layout.fragment_view_image, container, false)
}
@ -97,7 +97,7 @@ class ViewImageFragment : ViewMediaFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val arguments = this.arguments!!
val arguments = this.requireArguments()
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
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.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
@ -131,7 +132,11 @@ public final class ViewThreadFragment extends SFragment implements
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
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);
}

View File

@ -136,12 +136,12 @@ class ViewVideoFragment : ViewMediaFragment() {
progressBar.hide()
mp.isLooping = true
if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) {
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
videoView.start()
}
}
if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) {
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
mediaActivity.onBringUp()
}
}
@ -151,7 +151,7 @@ class ViewVideoFragment : ViewMediaFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = activity!!.toolbar
toolbar = requireActivity().toolbar
mediaActivity = activity as ViewMediaActivity
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
if (activeAccount != null) {
val notificationPref = requirePreference("notificationsEnabled") as SwitchPreferenceCompat
notificationPref.isChecked = activeAccount.notificationsEnabled
notificationPref.onPreferenceChangeListener = this
val mentionedPref = requirePreference("notificationFilterMentions") as SwitchPreferenceCompat
mentionedPref.isChecked = activeAccount.notificationsMentioned
mentionedPref.onPreferenceChangeListener = this
val followedPref = requirePreference("notificationFilterFollows") as SwitchPreferenceCompat
followedPref.isChecked = activeAccount.notificationsFollowed
followedPref.onPreferenceChangeListener = this
val boostedPref = requirePreference("notificationFilterReblogs") as SwitchPreferenceCompat
boostedPref.isChecked = activeAccount.notificationsReblogged
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
for (pair in mapOf(
"notificationsEnabled" to activeAccount.notificationsEnabled,
"notificationFilterMentions" to activeAccount.notificationsMentioned,
"notificationFilterFollows" to activeAccount.notificationsFollowed,
"notificationFilterFollowRequests" to activeAccount.notificationsFollowRequested,
"notificationFilterReblogs" to activeAccount.notificationsReblogged,
"notificationFilterFavourites" to activeAccount.notificationsFavorited,
"notificationFilterPolls" to activeAccount.notificationsPolls,
"notificationAlertSound" to activeAccount.notificationSound,
"notificationAlertVibrate" to activeAccount.notificationVibration,
"notificationAlertLight" to activeAccount.notificationLight
)) {
(requirePreference(pair.key) as SwitchPreferenceCompat).apply {
isChecked = pair.value
onPreferenceChangeListener = this@NotificationPreferencesFragment
}
}
}
}
@ -96,6 +77,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
}
"notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean
"notificationFilterFollows" -> activeAccount.notificationsFollowed = newValue as Boolean
"notificationFilterFollowRequests" -> activeAccount.notificationsFollowRequested = newValue as Boolean
"notificationFilterReblogs" -> activeAccount.notificationsReblogged = newValue as Boolean
"notificationFilterFavourites" -> activeAccount.notificationsFavorited = 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.SpannedString;
import androidx.core.text.HtmlCompat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
@ -25,7 +27,6 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.lang.reflect.Type;
@ -35,7 +36,9 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
throws JsonParseException {
String string = json.getAsString();
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 {
return new SpannedString("");
}
@ -43,6 +46,14 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerial
@Override
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
): 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")
fun scheduledStatuses(
@Query("limit") limit: Int? = null,
@ -383,6 +393,16 @@ interface MastodonApi {
@Path("id") accountId: String
): 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
@POST("api/v1/apps")
fun authenticateApp(

View File

@ -41,7 +41,7 @@ interface TimelineCases {
fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean)
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
fun muteConversation(status: Status, mute: Boolean): Single<Status>
}
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) {
val call = mastodonApi.muteAccount(id)
call.enqueue(object : Callback<Relationship> {

View File

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.repository
import android.text.SpannedString
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
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.NETWORK
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HtmlConverter
import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.inc
import io.reactivex.Single
@ -41,8 +42,7 @@ class TimelineRepositoryImpl(
private val timelineDao: TimelineDao,
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val gson: Gson,
private val htmlConverter: HtmlConverter
private val gson: Gson
) : TimelineRepository {
init {
@ -67,7 +67,7 @@ class TimelineRepositoryImpl(
val accountId = acc.id
timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson),
status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson)
)
@ -162,7 +162,7 @@ class TimelineRepositoryImpl(
for (status in statuses) {
timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson),
status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson)
)
@ -226,7 +226,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId,
reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""),
content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt),
emojis = emojis,
reblogsCount = status.reblogsCount,
@ -241,6 +241,7 @@ class TimelineRepositoryImpl(
mentions = mentions,
application = application,
pinned = false,
muted = status.muted,
poll = poll,
card = null,
quote = null
@ -269,6 +270,7 @@ class TimelineRepositoryImpl(
mentions = arrayOf(),
application = null,
pinned = false,
muted = status.muted,
poll = null,
card = null,
quote = null
@ -281,7 +283,7 @@ class TimelineRepositoryImpl(
inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId,
reblog = null,
content = status.content?.let(htmlConverter::fromHtml) ?: SpannedString(""),
content = status.content?.parseAsHtml() ?: SpannedString(""),
createdAt = Date(status.createdAt),
emojis = emojis,
reblogsCount = status.reblogsCount,
@ -296,6 +298,7 @@ class TimelineRepositoryImpl(
mentions = mentions,
application = application,
pinned = false,
muted = status.muted,
poll = poll,
card = null,
quote = null
@ -368,12 +371,12 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
application = null,
reblogServerId = null,
reblogAccountId = null,
poll = null
poll = null,
muted = false
)
}
fun Status.toEntity(timelineUserId: Long,
htmlConverter: HtmlConverter,
gson: Gson): TimelineStatusEntity {
val actionable = actionableStatus
return TimelineStatusEntity(
@ -383,7 +386,7 @@ fun Status.toEntity(timelineUserId: Long,
authorServerId = actionable.account.id,
inReplyToId = actionable.inReplyToId,
inReplyToAccountId = actionable.inReplyToAccountId,
content = htmlConverter.toHtml(actionable.content),
content = actionable.content.toHtml(),
createdAt = actionable.createdAt.time,
emojis = actionable.emojis.let(gson::toJson),
reblogsCount = actionable.reblogsCount,
@ -396,10 +399,11 @@ fun Status.toEntity(timelineUserId: Long,
visibility = actionable.visibility,
attachments = actionable.attachments.let(gson::toJson),
mentions = actionable.mentions.let(gson::toJson),
application = actionable.let(gson::toJson),
application = actionable.application.let(gson::toJson),
reblogServerId = reblog?.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.favouritesCount > 0) info.addAction(openFavsAction)
info.addAction(moreAction)
}
}
@ -150,6 +152,9 @@ class ListStatusAccessibilityDelegate(
interrupt()
statusActionListener.onShowFavs(pos)
}
R.id.action_more -> {
statusActionListener.onMore(host, pos)
}
else -> return super.performAccessibilityAction(host, action, args)
}
return true
@ -311,5 +316,10 @@ class ListStatusAccessibilityDelegate(
R.id.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)
}

View File

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

View File

@ -10,5 +10,9 @@ data class StatusDisplayOptions(
@get:JvmName("showBotOverlay")
val showBotOverlay: Boolean,
@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 androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
@ -172,15 +171,12 @@ class StatusViewHelper(private val itemView: View) {
sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE
} else {
val hiddenContentText: String = if (sensitive) {
sensitiveMediaWarning.text = if (sensitive) {
context.getString(R.string.status_sensitive_media_title)
} else {
context.getString(R.string.status_media_hidden_title)
}
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
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 {
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 pollDurationInfo: CharSequence
if (poll.expired) {
pollDurationInfo = context.getString(R.string.poll_info_closed)
val votesText = if(poll.votersCount == null) {
val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes)
} 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 {
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 {
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) {
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)
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.text.SpannableStringBuilder
import android.text.Spanned
import androidx.core.text.parseAsHtml
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.util.HtmlUtils
import java.util.*
import kotlin.math.roundToInt
@ -31,6 +31,7 @@ data class PollViewData(
val expired: Boolean,
val multiple: Boolean,
val votesCount: Int,
val votersCount: Int?,
val options: List<PollOptionViewData>,
var voted: Boolean
)
@ -41,16 +42,17 @@ data class PollOptionViewData(
var selected: Boolean
)
fun calculatePercent(fraction: Int, total: Int): Int {
fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
return if (fraction == 0) {
0
} else {
val total = totalVoters ?: totalVotes
(fraction / total.toDouble() * 100).roundToInt()
}
}
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(title)
}
@ -58,20 +60,21 @@ fun buildDescription(title: String, percent: Int, context: Context): Spanned {
fun Poll?.toViewData(): PollViewData? {
if (this == null) return null
return PollViewData(
id,
expiresAt,
expired,
multiple,
votesCount,
options.map { it.toViewData() },
voted
id = id,
expiresAt = expiresAt,
expired = expired,
multiple = multiple,
votesCount = votesCount,
votersCount = votersCount,
options = options.map { it.toViewData() },
voted = voted
)
}
fun PollOption.toViewData(): PollOptionViewData {
return PollOptionViewData(
title,
votesCount,
false
title = title,
votesCount = votesCount,
selected = false
)
}

View File

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

View File

@ -255,10 +255,10 @@
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:elevation="12dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="52dp"
android:paddingStart="24dp"
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="60dp"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
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"
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
android:id="@+id/button_toggle_content"
style="@style/TuskyButton.Outlined"
@ -176,7 +243,7 @@
android:textSize="?attr/status_text_medium"
android:visibility="gone"
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:visibility="visible" />

View File

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

View File

@ -1,55 +1,50 @@
<merge 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"
tools:parentTag="android.widget.LinearLayout">
tools:layout_height="wrap_content"
tools:layout_width="match_parent"
tools:parentTag="RadioGroup">
<RadioGroup
android:id="@+id/visibilityRadioGroup"
<RadioButton
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_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: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
android:id="@+id/publicRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_weight="1"
android:paddingEnd="0dp"
android:paddingStart="10dp"
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_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
android:id="@+id/privateRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_weight="1"
android:button="@drawable/ic_lock_outline_24dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_private"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
<RadioButton
android:id="@+id/unleakableRadioButton"
@ -58,8 +53,9 @@
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:paddingEnd="0dp"
android:button="@drawable/ic_reblog_unleakable_24dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_unleakable"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color"
@ -71,12 +67,11 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:paddingEnd="0dp"
android:button="@drawable/ic_email_24dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:text="@string/visibility_direct"
android:textColor="?android:textColorTertiary"
app:buttonTint="@color/compound_button_color" />
</RadioGroup>
</merge>

View File

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

View File

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

View File

@ -2,82 +2,82 @@
<resources>
<string name="error_generic">وقع هناك خطأ.</string>
<string name="error_network">حدث خطأ في الشبكة! يرجى التحقق من اتصالك ثم أعد المحاولة!</string>
<string name="error_empty">لا يجب أن يترك فارغا.</string>
<string name="error_invalid_domain">اسم النطاق غير صالح</string>
<string name="error_empty">لا يجب أن يترك هذا الحقل فارغا.</string>
<string name="error_invalid_domain">اسم النطاق الذي قمتَ بإدخاله غير صالح</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_denied">تم رفض التصريح.</string>
<string name="error_retrieving_oauth_token">فشل الحصول على رمز الدخول.</string>
<string name="error_compose_character_limit">المنشور طويل جدا !</string>
<string name="error_image_upload_size">يجب أن يكون حجم الملف أقل من 4 ميغابايت.</string>
<string name="error_retrieving_oauth_token">فشل الحصول على رمز الولوج.</string>
<string name="error_compose_character_limit">إنّ المنشور طويل جدا!</string>
<string name="error_image_upload_size">يجب أن يكون حجم الملف أقل من 8 ميغابايت.</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_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_sender_account_gone">خطأ عند إرسال التبويق.</string>
<string name="title_home">الرئيسية</string>
<string name="title_home">الرئيسي</string>
<string name="title_notifications">الاشعارات</string>
<string name="title_public_local">المحلية</string>
<string name="title_public_federated">الفدرالية</string>
<string name="title_public_local">المحلي</string>
<string name="title_public_federated">الفدرالي</string>
<string name="title_direct_messages">الرسائل المباشرة</string>
<string name="title_tab_preferences">الألسنة</string>
<string name="title_view_thread">تبويق</string>
<string name="title_statuses">المشاركات</string>
<string name="title_statuses_with_replies">يحتوي على ردود</string>
<string name="title_statuses_pinned">مدبّس</string>
<string name="title_statuses">المنشورات</string>
<string name="title_statuses_with_replies">التبويقات والردود</string>
<string name="title_statuses_pinned">المدبّسة</string>
<string name="title_follows">المتابَعون</string>
<string name="title_followers">المتابِعون</string>
<string name="title_favourites">المفضلة</string>
<string name="title_mutes">الحسابات المكتومة</string>
<string name="title_blocks">الحسابات المحظورة</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_licenses">الرّخص</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_media_hidden_title">وسائط مخفية</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_show_more">توسيع</string>
<string name="status_content_show_less">تصغير</string>
<string name="message_empty">لا شيء هنا.</string>
<string name="footer_empty">لا يوجد شيئ هنا. إسحب إلى أسفل للتحديث !</string>
<string name="notification_reblog_format">رقّى %s تبويقك</string>
<string name="footer_empty">لا يوجد شيء هنا. إسحب إلى أسفل للإنعاش!</string>
<string name="notification_reblog_format">شارَك %s تبويقك</string>
<string name="notification_favourite_format">أعجِب %s بتبويقك</string>
<string name="notification_follow_format">%s يتبعك</string>
<string name="report_username_format">أبلغ عن @%s</string>
<string name="report_comment_hint">تعليقات إضافية ؟</string>
<string name="action_quick_reply">إجابة سريعة</string>
<string name="action_reply">أجب</string>
<string name="report_comment_hint">تعليقات إضافية؟</string>
<string name="action_quick_reply">رد سريع</string>
<string name="action_reply">رد</string>
<string name="action_reblog">رقّي</string>
<string name="action_unreblog">إزالة الترقية</string>
<string name="action_favourite">تفضيل</string>
<string name="action_unfavourite">إزالة المفضلة</string>
<string name="action_more">المزيد</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_confirm">متأكد مِن أنك تود الخروج من الحساب %1$s ؟</string>
<string name="action_logout_confirm">متأكد مِن أنك تود الخروج من الحساب %1$s؟</string>
<string name="action_follow">إتبع</string>
<string name="action_unfollow">إلغاء التتبع</string>
<string name="action_unfollow">إلغاء المتابعة</string>
<string name="action_block">قم بحظره</string>
<string name="action_unblock">إلغاء الحظر</string>
<string name="action_hide_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_send">بَوّق</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_view_profile">الملف الشخصي</string>
<string name="action_view_profile">الملف التعريفي</string>
<string name="action_view_preferences">التفضيلات</string>
<string name="action_view_account_preferences">تفضيلات الحساب</string>
<string name="action_view_favourites">المفضلة</string>
@ -95,7 +95,7 @@
<string name="action_hide_media">إخفاء الوسائط</string>
<string name="action_open_drawer">إفتح الدرج</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_undo">إلغاء</string>
<string name="action_accept">موافقة</string>
@ -109,8 +109,8 @@
<string name="action_links">الروابط</string>
<string name="action_mentions">الإشارات</string>
<string name="action_hashtags">الوسوم</string>
<string name="action_open_reblogged_by">عرض الترقيات</string>
<string name="action_open_faved_by">عرض المفضلات</string>
<string name="action_open_reblogged_by">اعرض الترقيات</string>
<string name="action_open_faved_by">اعرض المفضلات</string>
<string name="title_hashtags_dialog">الوسوم</string>
<string name="title_mentions_dialog">الإشارات</string>
<string name="title_links_dialog">الروابط</string>
@ -120,23 +120,23 @@
<string name="action_share_as">شاركه كـ…</string>
<string name="send_status_link_to">شارك رابط التبويق مع…</string>
<string name="send_status_content_to">شارك التبويق على…</string>
<string name="send_media_to">شارك رابط التبويق مع…</string>
<string name="confirmation_reported">تم الإرسال !</string>
<string name="send_media_to">شارك الوسيط مع…</string>
<string name="confirmation_reported">تم إرساله!</string>
<string name="confirmation_unblocked">تم فك الحجب عن الحساب</string>
<string name="confirmation_unmuted">تم فك الكتم عن الحساب</string>
<string name="status_sent">تم إرساله !</string>
<string name="confirmation_unmuted">لم يعد الحساب مكتومًا</string>
<string name="status_sent">تم إرساله!</string>
<string name="status_sent_long">تم إرسال الرد بنجاح.</string>
<string name="hint_domain">أي سيرفر ؟</string>
<string name="hint_compose">ما الجديد ؟</string>
<string name="hint_domain">أي مثيل خادم؟</string>
<string name="hint_compose">ما الجديد؟</string>
<string name="hint_content_warning">تحذير عن المحتوى</string>
<string name="hint_display_name">الإسم العلني</string>
<string name="hint_note">السيرة</string>
<string name="hint_search">البحث عن…</string>
<string name="search_no_results">لم يتم العثور على نتائج</string>
<string name="label_quick_reply">إجابة </string>
<string name="label_avatar">الصورة الرمزية</string>
<string name="label_header">رأس الصفحة</string>
<string name="link_whats_an_instance">ماذا نعني بمثيل الخادم ؟</string>
<string name="search_no_results">لم يتم العثور على أية نتائج</string>
<string name="label_quick_reply">رد</string>
<string name="label_avatar">صورة الملف التعريفي</string>
<string name="label_header">صورة رأس الصفحة</string>
<string name="link_whats_an_instance">ماذا نعني بمثيل الخادم؟</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>
\n
@ -148,13 +148,13 @@
<string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string>
<string name="dialog_message_uploading_media">الإرسال جارٍ…</string>
<string name="dialog_download_image">تنزيل</string>
<string name="dialog_message_cancel_follow_request">هل تريد رفض طلب المتابعة ؟</string>
<string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب ؟</string>
<string name="dialog_message_cancel_follow_request">هل تريد رفض طلب المتابعة؟</string>
<string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب؟</string>
<string name="dialog_delete_toot_warning">هل تريد حذف هذا التبويق؟</string>
<string name="visibility_public">عمومي : ينشر على الخيوط العمومية</string>
<string name="visibility_unlisted">غير مدرج : لا يُعرَض على الخيوط العمومية</string>
<string name="visibility_private">لمتابعيك فقط : يُنشر إلى متابعيك فقط</string>
<string name="visibility_direct">مباشر : يُنشر إلى المستخدمين المشار إليهم فقط</string>
<string name="visibility_public">للعامة: ينشر على الخيوط العمومية</string>
<string name="visibility_unlisted">غير مدرج: لا يُعرَض على الخيوط العمومية</string>
<string name="visibility_private">لمتابعيك فقط: يُنشر إلى متابعيك فقط</string>
<string name="visibility_direct">مباشر: يُنشر إلى المستخدمين المشار إليهم فقط</string>
<string name="pref_title_edit_notification_settings">تعديل الاشعارات</string>
<string name="pref_title_notifications_enabled">الإخطارات</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_filters">أخطرني عندما</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_favourites">أعجب أحد ما بمنشوراتي</string>
<string name="pref_title_notification_filter_favourites">يُعجَب أحد ما بمنشوراتي</string>
<string name="pref_title_appearance_settings">المظهر</string>
<string name="pref_title_app_theme">سمة التطبيق</string>
<string name="pref_title_timelines">الخيوط</string>
<string name="pref_title_app_theme">حُلّة التطبيق</string>
<string name="pref_title_timelines">الخيوط الزمنية</string>
<string name="pref_title_timeline_filters">عوامل التصفية</string>
<string name="app_them_dark">داكنة</string>
<string name="app_theme_light">فاتحة</string>
@ -339,7 +339,7 @@
<string name="action_open_reblogger">إظهار صاحب الترقية</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="dialog_redraft_toot_warning">هل تريد حذف وإعادة صياغة هذا التبويق؟</string>
@ -378,7 +378,7 @@
<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">
<item quantity="zero"><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="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_remote_instance">هذا الحساب ينتسب إلى خادم آخر. هل تريد إرسال نسخة مجهولة من التقرير إلى هناك أيضا؟</string>
@ -501,4 +501,6 @@
<string name="no_scheduled_status">ليس لديك أية منشورات مُبرمَجة للنشر.</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>
</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="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_empty">Este campo no puede estar vacío.</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_authorization_unknown">Ocurrió un error de autorización no identificado.</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="list">Lista</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_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_licenses">Licences</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_media_hidden_title">Média caché</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="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="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_follow_format">%s vous suit</string>
<string name="report_username_format">Signaler @%s</string>
<string name="report_comment_hint">Commentaires additonnels ?</string>
<string name="action_quick_reply">Réponse rapide</string>
<string name="action_reply">Répondre</string>
<string name="action_reblog">Booster</string>
<string name="action_unreblog">Supprimer le boost</string>
<string name="action_reblog">Partager</string>
<string name="action_unreblog">Annuler le partage</string>
<string name="action_favourite">Favori</string>
<string name="action_unfavourite">Supprimer le favori</string>
<string name="action_more">Plus</string>
@ -69,8 +69,8 @@
<string name="action_unfollow">Ne plus suivre</string>
<string name="action_block">Bloquer</string>
<string name="action_unblock">Débloquer</string>
<string name="action_hide_reblogs">Cacher les boosts</string>
<string name="action_show_reblogs">Montrer les boosts</string>
<string name="action_hide_reblogs">Cacher les partages</string>
<string name="action_show_reblogs">Montrer les partages</string>
<string name="action_report">Signaler</string>
<string name="action_delete">Supprimer</string>
<string name="action_send">POUET</string>
@ -109,8 +109,8 @@
<string name="action_links">Liens</string>
<string name="action_mentions">Mentions</string>
<string name="action_hashtags">Hashtags</string>
<string name="action_open_reblogger">Afficher lauteur·rice du boost</string>
<string name="action_open_reblogged_by">Afficher les boosts</string>
<string name="action_open_reblogger">Afficher lauteur·rice du partage</string>
<string name="action_open_reblogged_by">Montrer les partages</string>
<string name="action_open_faved_by">Montrer les favoris</string>
<string name="title_hashtags_dialog">Hashtags</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_filter_mentions">on me mentionne</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_appearance_settings">Apparence</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_status_filter">Filtrage des fils</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_media_preview">Montrer les miniatures des médias</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_description">Notifications pour les nouveaux abonnés</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_description">Notifications quand vos pouets sont mis en favoris</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="profile_badge_bot_text">Robot</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="unreblog_private">Ne plus booster</string>
<string name="reblog_private">Partager à laudience originale</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_apache_2">Sous licence Apache (copie ci-dessous)</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>
</plurals>
<plurals name="reblogs">
<item quantity="one">&lt;b&gt;%s&lt;/b&gt; Boost</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Boosts</item>
</plurals>
<string name="title_reblogged_by">Boosté par</string>
<item quantity="one"><b>%s</b> Partage</item>
<item quantity="other"><b>%s</b> Partages</item>
</plurals>
<string name="title_reblogged_by">Partagé par</string>
<string name="title_favourited_by">Mis en favoris par</string>
<string name="conversation_1_recipients">%1$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="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="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_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>

View File

@ -508,4 +508,6 @@
<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>
</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="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>

View File

@ -1,22 +1,22 @@
<?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_saved_toot">Irewwayen</string>
<string name="action_logout">Ffeγ</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_edit_profile">Ẓreg amaγnu</string>
<string name="action_logout">Ffeɣ</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_edit_profile">Ẓreg amaɣnu</string>
<string name="action_search">Nadi</string>
<string name="about_title_activity">Γef</string>
<string name="action_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="about_title_activity">Ɣef</string>
<string name="action_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="title_home">Agejdan</string>
<string name="title_tab_preferences">Iccaren</string>
<string name="title_view_thread">Tijewwiqt</string>
<string name="title_statuses">Iznan</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_content_warning_show_more">Zeṛ ugar</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_retry">Ɛreḍ tikkelt-nniḍen</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_open_in_web">Ldi deg uminig</string>
<string name="action_share">Bḍu</string>
<string name="action_mute">Sgugem</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>
<plurals name="favs">
@ -51,11 +51,11 @@
<string name="no_saved_status">Ur tesɛiḍ ara irewwayen.</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="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_mute_domain">Sgugem %s</string>
<string name="action_mention">Bder</string>
@ -64,20 +64,20 @@
<string name="action_undo">Sefsex</string>
<string name="action_emoji_keyboard">Anasiw n imujiyen</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_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="hint_domain">Anta tummant\?</string>
<string name="hint_compose">d-acu i gellan d amaynut\?</string>
<string name="hint_search">Nadi…</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_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="app_theme_light">Aceɛlal</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_small">%1$s akked %2$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_video">Tibidyutin</string>
@ -109,17 +109,17 @@
<string name="pin_action">Senṭeḍ</string>
<string name="action_view_mutes">Imiḍanen yettwasgugmen</string>
<string name="action_view_blocks">Imiḍanen yettusḥebsen</string>
<string name="action_view_domain_mutes">Tiγula yettwaffren</string>
<string name="action_view_blocks">Imiḍan yettwacekklen</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_media">Taγwalt</string>
<string name="action_view_media">Taɣwalt</string>
<string name="notifications_clear">Sfeḍ</string>
<string name="title_mutes">Imiḍanen yettwasgugmen</string>
<string name="title_blocks">Imiḍanen yettusḥebsen</string>
<string name="title_domain_mutes">Tiγula yettwaffren</string>
<string name="title_blocks">Imiḍanen yettwacekklen</string>
<string name="title_domain_mutes">Tiɣula yettwaffren</string>
<string name="title_follow_requests">Isuturen n teḍfeṛt</string>
<string name="pref_title_notifications_enabled">Ẓreg tilγa</string>
<string name="title_media">Taγwalt</string>
<string name="pref_title_notifications_enabled">Tilɣa</string>
<string name="title_media">Taywalt</string>
<string name="action_remove">Kkes</string>
<string name="compose_shortcut_short_label">Azen</string>
@ -129,28 +129,28 @@
<string name="action_add_poll">Rnu assenqed</string>
<string name="action_photo_take">Ṭef tugna</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_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_edit_dialog_title">Ẓreg amsizdeg</string>
<string name="action_create_list">Snulfu-d umuγ</string>
<string name="action_rename_list">Snifel isem n wumuγ</string>
<string name="action_delete_list">Kkes 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_remove_from_list">Kkes amiḍan seg wumuγ</string>
<string name="action_create_list">Snulfu-d umuɣ</string>
<string name="action_rename_list">Snifel isem n wumuɣ</string>
<string name="action_delete_list">Kkes umuɣ-a</string>
<string name="action_edit_list">Ẓreg umuɣ-a</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="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="list">Umuγ</string>
<string name="list">Umuɣ</string>
<string name="notifications_apply_filter">Sizdeg</string>
<string name="title_accounts">Imiḍanen</string>
<string name="add_poll_choice">Rnu yiwen wefran</string>
<string name="report_username_format">Ccetki γef @%s</string>
<string name="action_report">Ccetki</string>
<string name="report_username_format">Ccetki ɣef @%s</string>
<string name="action_report">Ccetki fell-as</string>
<string name="action_reject">Ggami</string>
<string name="download_image">Yessidired %1$s</string>
@ -159,16 +159,16 @@
<string name="login_connection">itteqqen…</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="app_theme_auto">Akken yella yiṭij</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_server">Tansa n upṛuksi HTTP</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="description_account_locked">Yettwargel umiḍan</string>
@ -183,26 +183,26 @@
<string name="restart">Ales tanekra</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="conversation_1_recipients">%1$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="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">
<item quantity="one">%s wedγar</item>
<item quantity="other">%s n yedγaren</item>
<item quantity="one">%s n wedɣar</item>
<item quantity="other">%s n yedɣaren</item>
</plurals>
<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_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_created">Ifukk yiwen wedγar id snulfaḍ</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>
<plurals name="poll_timespan_days">
<item quantity="one">%d n wass</item>
@ -214,18 +214,18 @@
</plurals>
<plurals name="poll_timespan_minutes">
<item quantity="one">%d n tasdidt</item>
<item quantity="other">%d n tesdidin</item>
<item quantity="other">%d n tisdidin</item>
</plurals>
<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_search">Tucḍa n unadi</string>
<string name="create_poll_title">Assenqed</string>
<string name="poll_duration_5_min">5 n tasditin</string>
<string name="poll_duration_30_min">30 n tasditin</string>
<string name="poll_duration_1_hour">1 n wesrag</string>
<string name="poll_duration_6_hours">6 n wesragen</string>
<string name="poll_duration_5_min">5 n tisdidin</string>
<string name="poll_duration_30_min">30 n tisdidin</string>
<string name="poll_duration_1_hour">1 n usrag</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_3_days">3 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_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="action_links">Iseγwan</string>
<string name="action_links">Iseɣwan</string>
<string name="action_mentions">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="status_sent">Yettwaceyyaɛ!</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="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_project_site">Asmel Web n usenfaṛ:
\n https://tusky.app</string>
<string name="abbreviated_hours_ago">%dasr</string>
<string name="abbreviated_minutes_ago">%dtas</string>
<string name="abbreviated_seconds_ago">%dtasn</string>
<string name="abbreviated_hours_ago">%dsr</string>
<string name="abbreviated_minutes_ago">%dtsd</string>
<string name="abbreviated_seconds_ago">%dtsn</string>
<string name="compose_save_draft">Sekles amzun d arewway\?</string>
<string name="later">Ticki</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>

View File

@ -520,4 +520,6 @@
<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>
</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_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_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>

View File

@ -486,4 +486,5 @@
<string name="error_audio_upload_size">Áudios devem ser menores que 40MB.</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>

View File

@ -546,4 +546,13 @@
<string name="description_status_bookmarked">Добавлено в закладки</string>
<string name="select_list_title">Выбрать список</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_sender_account_gone">Kunde inte skicka toot.</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_federated">Federerat</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_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="pref_title_edit_notification_settings">Notifikationer</string>
<string name="pref_title_notifications_enabled">Notifikationer</string>
<string name="pref_title_edit_notification_settings">Aviseringar</string>
<string name="pref_title_notifications_enabled">Aviseringar</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_vibrate">Meddela med vibration</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_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_favourites">mina inlägg är favoriserade</string>
<string name="pref_title_appearance_settings">Utseende</string>
@ -177,7 +177,7 @@
<string name="app_theme_light">Ljust</string>
<string name="app_theme_black">Svart</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_custom_tabs">Använd Chrome-anpassade flikar</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_largest">Största</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_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_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_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_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>
@ -239,7 +239,7 @@
<string name="status_share_link">Dela länk till toot</string>
<string name="status_media_images">Bilder</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"-->
<string name="abbreviated_in_years">om %dy</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_delete_list">Kunde inte radera lista</string>
<string name="action_create_list">Skapa en lista</string>
<string name="action_rename_list">Byt namn</string>
<string name="action_delete_list">Ta bort</string>
<string name="action_edit_list">Ändra</string>
<string name="action_rename_list">Byt namn på listan</string>
<string name="action_delete_list">Ta bort listan</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="action_add_to_list">Lägg till konto i 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="compose_save_draft">Spara utkast?</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_cancel_title">Sändning avbruten</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="download_fonts">Du behöver ladda ned dessa emojis först</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="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="later">Senare</string>
<string name="restart">Starta om</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="download_failed">Nedladdning misslyckad</string>
<string name="profile_badge_bot_text">Robot</string>
@ -373,9 +373,9 @@
<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="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>
</plurals>
<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_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_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>
@ -424,9 +424,9 @@
<string name="title_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="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="caption_notoemoji">Google\'s nuvarande emojis</string>
@ -443,7 +443,7 @@
<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_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="title_accounts">Konton</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="no_saved_status">Du har inga utkast.</string>
<string name="warning_scheduling_interval">Mastodon har ett minimalt schemaläggningsintervall på 5 minuter.</string>
</resources>

View File

@ -161,7 +161,7 @@
<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="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="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string>
<string name="dialog_whats_an_instance">"請輸入你帳號所在的 Mastodon 站點的域名或地址 "</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_reblogged_by" type="id" />
<item name="action_open_faved_by" type="id" />
<item name="action_more" type="id" />
</resources>

View File

@ -7,7 +7,6 @@
<dimen name="compose_media_preview_margin">8dp</dimen>
<dimen name="compose_media_preview_margin_bottom">0dp</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="tab_page_margin">16dp</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_favourite_format">%s favorited your toot</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_comment_hint">Additional comments?</string>
@ -113,6 +114,8 @@
<string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</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_hide_media">Hide media</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="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="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_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_filter_mentions">mentioned</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_favourites">my posts are favorited</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_follow_name">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_description">Notifications when your toots get boosted</string>
<string name="notification_favourite_name">Favorites</string>
@ -516,6 +524,10 @@
<item quantity="one">%s vote</item>
<item quantity="other">%s votes</item>
</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_absolute">ends at %s</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_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="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>

View File

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

View File

@ -72,6 +72,18 @@
android:title="@string/pref_title_show_notifications_filter"
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 android:title="@string/pref_title_limited_bandwidth_settings">

View File

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

View File

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

View File

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

View File

@ -18,13 +18,9 @@ package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat.FileEmojiCompatConfig
import javax.inject.Inject
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() {

View File

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

View File

@ -1,11 +1,11 @@
buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.3.71'
repositories {
google()
jcenter()
}
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"
}
}

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