Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-12-31 09:35:22 +09:00
commit 2ea8ee6bc8
50 changed files with 1176 additions and 233 deletions

View File

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

View File

@ -57,6 +57,7 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
@ -84,6 +85,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private var muting: Boolean = false private var muting: Boolean = false
private var blockingDomain: Boolean = false private var blockingDomain: Boolean = false
private var showingReblogs: Boolean = false private var showingReblogs: Boolean = false
private var subscribing: Boolean = false
private var loadedAccount: Account? = null private var loadedAccount: Account? = null
private var animateAvatar: Boolean = false private var animateAvatar: Boolean = false
@ -116,7 +118,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadResources() loadResources()
makeNotificationBarTransparent() makeNotificationBarTransparent()
setContentView(R.layout.activity_account) setContentView(R.layout.activity_account)
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
@ -159,7 +161,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountMuteButton.hide() accountMuteButton.hide()
accountFollowsYouTextView.hide() accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields // setup the RecyclerView for the account fields
accountFieldList.isNestedScrollingEnabled = false accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.layoutManager = LinearLayoutManager(this)
@ -186,6 +187,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
} }
// If wellbeing mode is enabled, follow stats and posts count should be hidden
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
if (wellbeingEnabled) {
accountStatuses.hide()
accountFollowers.hide()
accountFollowing.hide()
}
} }
/** /**
@ -200,8 +211,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { tab, position ->
tab, position ->
tab.text = pageTitles[position] tab.text = pageTitles[position]
}.attach() }.attach()
@ -374,7 +384,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
accountLockedImageView.visible(account.locked) accountLockedImageView.visible(account.locked)
accountBadgeTextView.visible(account.bot) accountBadgeTextView.visible(account.bot)
@ -536,7 +545,25 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
blockingDomain = relation.blockingDomain blockingDomain = relation.blockingDomain
showingReblogs = relation.showingReblogs showingReblogs = relation.showingReblogs
accountFollowsYouTextView.visible(relation.followedBy) // If wellbeing mode is enabled, "follows you" text should not be visible
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
&& (relation.subscribing != null || relation.notifying != null)) {
accountSubscribeButton.show()
accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState()
}
if(relation.notifying != null)
subscribing = relation.notifying
else if(relation.subscribing != null)
subscribing = relation.subscribing
}
accountNoteTextInputLayout.visible(relation.note != null) accountNoteTextInputLayout.visible(relation.note != null)
accountNoteTextInputLayout.editText?.setText(relation.note) accountNoteTextInputLayout.editText?.setText(relation.note)
@ -574,6 +601,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFollowButton.setText(R.string.action_unfollow) accountFollowButton.setText(R.string.action_unfollow)
} }
} }
updateSubscribeButton()
} }
private fun updateMuteButton() { private fun updateMuteButton() {
@ -584,6 +612,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} }
private fun updateSubscribeButton() {
if(followState != FollowState.FOLLOWING) {
accountSubscribeButton.hide()
}
if(subscribing) {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
} else {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp)
}
}
private fun updateButtons() { private fun updateButtons() {
invalidateOptionsMenu() invalidateOptionsMenu()
@ -595,6 +635,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (blocking || viewModel.isSelf) { if (blocking || viewModel.isSelf) {
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountMuteButton.hide() accountMuteButton.hide()
accountSubscribeButton.hide()
} else { } else {
accountFloatingActionButton.show() accountFloatingActionButton.show()
if (muting) if (muting)
@ -603,12 +644,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountMuteButton.hide() accountMuteButton.hide()
updateMuteButton() updateMuteButton()
} }
//} else {
//accountFloatingActionButton.hide()
//accountFollowButton.hide()
//accountMuteButton.hide()
//}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -722,8 +757,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (viewModel.relationshipData.value?.data?.muting != true) { if (viewModel.relationshipData.value?.data?.muting != true) {
loadedAccount?.let { loadedAccount?.let {
showMuteAccountDialog( showMuteAccountDialog(
this, this,
it.username it.username
) { notifications -> ) { notifications ->
viewModel.muteAccount(notifications) viewModel.muteAccount(notifications)
} }

View File

@ -38,6 +38,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
@ -202,8 +203,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
holder.setUsername(statusViewData.getNickname()); holder.setUsername(statusViewData.getNickname());
holder.setCreatedAt(statusViewData.getCreatedAt()); holder.setCreatedAt(statusViewData.getCreatedAt());
holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(), if(concreteNotificaton.getType() == Notification.Type.STATUS) {
concreteNotificaton.getAccount().getAvatar()); holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
} else {
holder.setAvatars(statusViewData.getAvatar(),
concreteNotificaton.getAccount().getAvatar());
}
} }
holder.setMessage(concreteNotificaton, statusListener); holder.setMessage(concreteNotificaton, statusListener);
@ -254,6 +259,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
CardViewMode.NONE, CardViewMode.NONE,
statusDisplayOptions.confirmReblogs(), statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats(),
statusDisplayOptions.quoteEnabled() statusDisplayOptions.quoteEnabled()
); );
} }
@ -272,6 +278,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case POLL: { case POLL: {
return VIEW_TYPE_STATUS; return VIEW_TYPE_STATUS;
} }
case STATUS:
case FAVOURITE: case FAVOURITE:
case REBLOG: { case REBLOG: {
return VIEW_TYPE_STATUS_NOTIFICATION; return VIEW_TYPE_STATUS_NOTIFICATION;
@ -380,6 +387,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private SimpleDateFormat shortSdf; private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf; private SimpleDateFormat longSdf;
private int avatarRadius48dp;
private int avatarRadius36dp;
private int avatarRadius24dp;
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView); super(itemView);
message = itemView.findViewById(R.id.notification_top_text); message = itemView.findViewById(R.id.notification_top_text);
@ -406,6 +417,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusContent.setOnClickListener(this); statusContent.setOnClickListener(this);
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
} }
private void showNotificationContent(boolean show) { private void showNotificationContent(boolean show) {
@ -496,6 +511,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
format = context.getString(R.string.notification_reblog_format); format = context.getString(R.string.notification_reblog_format);
break; break;
} }
case STATUS: {
icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp);
if (icon != null) {
icon.setColorFilter(ContextCompat.getColor(context,
R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP);
}
format = context.getString(R.string.notification_subscription_format);
break;
}
} }
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
String wholeMessage = String.format(format, displayName); String wholeMessage = String.format(format, displayName);
@ -534,19 +559,34 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.notificationId = notificationId; this.notificationId = notificationId;
} }
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) { void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) {
statusAvatar.setPaddingRelative(0, 0, 0, 0);
int statusAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_36dp);
ImageLoadingHelper.loadAvatar(statusAvatarUrl, ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars()); statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars());
int notificationAvatarRadius = statusAvatar.getContext().getResources() if (statusDisplayOptions.showBotOverlay() && isBot) {
.getDimensionPixelSize(R.dimen.avatar_radius_24dp); notificationAvatar.setVisibility(View.VISIBLE);
notificationAvatar.setBackgroundColor(0x50ffffff);
Glide.with(notificationAvatar)
.load(R.drawable.ic_bot_24dp)
.into(notificationAvatar);
} else {
notificationAvatar.setVisibility(View.GONE);
}
}
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
int padding = Utils.dpToPx(statusAvatar.getContext(), 12);
statusAvatar.setPaddingRelative(0, 0, padding, padding);
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars());
notificationAvatar.setVisibility(View.VISIBLE);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
notificationAvatarRadius, statusDisplayOptions.animateAvatars()); avatarRadius24dp, statusDisplayOptions.animateAvatars());
} }
private void setQuoteContainer(Status status, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) { private void setQuoteContainer(Status status, final LinkListener listener, StatusDisplayOptions statusDisplayOptions) {

View File

@ -112,7 +112,12 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
if (payloads == null) { if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
if (!statusDisplayOptions.hideStats()) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
} else {
hideQuantitativeStats();
}
setApplication(status.getApplication()); setApplication(status.getApplication());
@ -131,4 +136,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
contentWarningDescription.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener);
} }
} }
private void hideQuantitativeStats() {
reblogs.setVisibility(View.GONE);
favourites.setVisibility(View.GONE);
infoDivider.setVisibility(View.GONE);
}
} }

View File

@ -66,6 +66,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
statusDisplayOptions.cardViewMode(), statusDisplayOptions.cardViewMode(),
statusDisplayOptions.confirmReblogs(), statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats(),
statusDisplayOptions.quoteEnabled() statusDisplayOptions.quoteEnabled()
); );
} }

View File

@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import kotlinx.android.synthetic.main.item_announcement.view.* import kotlinx.android.synthetic.main.item_announcement.view.*
interface AnnouncementActionListener: LinkListener { interface AnnouncementActionListener: LinkListener {
fun openReactionPicker(announcementId: String, target: View) fun openReactionPicker(announcementId: String, target: View)
fun addReaction(announcementId: String, name: String) fun addReaction(announcementId: String, name: String)
@ -40,7 +41,8 @@ interface AnnouncementActionListener: LinkListener {
class AnnouncementAdapter( class AnnouncementAdapter(
private var items: List<Announcement> = emptyList(), private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() { ) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
@ -68,6 +70,14 @@ class AnnouncementAdapter(
fun bind(item: Announcement) { fun bind(item: Announcement) {
LinkHelper.setClickableText(text, item.content, null, listener, false) LinkHelper.setClickableText(text, item.content, null, listener, false)
// If wellbeing mode is enabled, announcement badge counts should not be shown.
if (wellbeingEnabled) {
// Since reactions are not visible in wellbeing mode,
// we shouldn't be able to add any ourselves.
addReactionChip.visibility = View.GONE
return
}
item.reactions.forEachIndexed { i, reaction -> item.reactions.forEachIndexed { i, reaction ->
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {

View File

@ -17,18 +17,23 @@ package com.keylesspalace.tusky.components.announcements
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.* import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker
import kotlinx.android.synthetic.main.activity_announcements.* import kotlinx.android.synthetic.main.activity_announcements.*
@ -42,7 +47,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
private val adapter = AnnouncementAdapter(emptyList(), this) private lateinit var adapter: AnnouncementAdapter
private val picker by lazy { EmojiPicker(this) } private val picker by lazy { EmojiPicker(this) }
private val pickerDialog by lazy { private val pickerDialog by lazy {
@ -75,6 +80,12 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
announcementsList.layoutManager = LinearLayoutManager(this) announcementsList.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
announcementsList.addItemDecoration(divider) announcementsList.addItemDecoration(divider)
val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled)
announcementsList.adapter = adapter announcementsList.adapter = adapter
viewModel.announcements.observe(this) { viewModel.announcements.observe(this) {

View File

@ -117,6 +117,7 @@ class ComposeActivity : BaseActivity(),
private var composeOptions: ComposeOptions? = null private var composeOptions: ComposeOptions? = null
private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private val maxUploadMediaNumber = 4
private var mediaCount = 0 private var mediaCount = 0
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
@ -906,6 +907,7 @@ class ComposeActivity : BaseActivity(),
val mimeTypes = arrayOf("image/*", "video/*", "audio/*") val mimeTypes = arrayOf("image/*", "video/*", "audio/*")
intent.type = "*/*" intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
startActivityForResult(intent, MEDIA_PICK_RESULT) startActivityForResult(intent, MEDIA_PICK_RESULT)
} }
@ -932,7 +934,23 @@ class ComposeActivity : BaseActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
pickMedia(intent.data!!) if(intent.data != null){
// Single media, upload it and done.
pickMedia(intent.data!!)
}else if(intent.clipData != null){
val clipData = intent.clipData!!
val count = clipData.itemCount
if(mediaCount + count > maxUploadMediaNumber){
// check if exist media + upcoming media > 4, then prob error message.
Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
}else{
// if not grater then 4, upload all multiple media.
for (i in 0 until count) {
val imageUri = clipData.getItemAt(i).getUri()
pickMedia(imageUri)
}
}
}
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
pickMedia(photoUploadUri!!) pickMedia(photoUploadUri!!)
} }

View File

@ -29,12 +29,12 @@ import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.components.compose.CAN_USE_QUOTE_ID import com.keylesspalace.tusky.components.compose.CAN_USE_QUOTE_ID
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -46,8 +46,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var db: AppDatabase
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
@ -70,6 +68,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID
) )

View File

@ -121,7 +121,7 @@ public class NotificationHelper {
public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; public static final String CHANNEL_BOOST = "CHANNEL_BOOST";
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_POLL = "CHANNEL_POLL";
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
/** /**
* WorkManager Tag * WorkManager Tag
@ -138,6 +138,7 @@ public class NotificationHelper {
*/ */
public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) { public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) {
body = body.rewriteToStatusTypeIfNeeded(account.getAccountId());
if (!filterNotification(account, body, context)) { if (!filterNotification(account, body, context)) {
return; return;
@ -355,6 +356,7 @@ public class NotificationHelper {
CHANNEL_BOOST + account.getIdentifier(), CHANNEL_BOOST + account.getIdentifier(),
CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(),
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
}; };
int[] channelNames = { int[] channelNames = {
R.string.notification_mention_name, R.string.notification_mention_name,
@ -362,7 +364,8 @@ public class NotificationHelper {
R.string.notification_follow_request_name, R.string.notification_follow_request_name,
R.string.notification_boost_name, R.string.notification_boost_name,
R.string.notification_favourite_name, R.string.notification_favourite_name,
R.string.notification_poll_name R.string.notification_poll_name,
R.string.notification_subscription_name,
}; };
int[] channelDescriptions = { int[] channelDescriptions = {
R.string.notification_mention_descriptions, R.string.notification_mention_descriptions,
@ -370,7 +373,8 @@ public class NotificationHelper {
R.string.notification_follow_request_description, R.string.notification_follow_request_description,
R.string.notification_boost_description, R.string.notification_boost_description,
R.string.notification_favourite_description, R.string.notification_favourite_description,
R.string.notification_poll_description R.string.notification_poll_description,
R.string.notification_subscription_description,
}; };
List<NotificationChannel> channels = new ArrayList<>(6); List<NotificationChannel> channels = new ArrayList<>(6);
@ -516,6 +520,8 @@ public class NotificationHelper {
switch (notification.getType()) { switch (notification.getType()) {
case MENTION: case MENTION:
return account.getNotificationsMentioned(); return account.getNotificationsMentioned();
case STATUS:
return account.getNotificationsSubscriptions();
case FOLLOW: case FOLLOW:
return account.getNotificationsFollowed(); return account.getNotificationsFollowed();
case FOLLOW_REQUEST: case FOLLOW_REQUEST:
@ -536,6 +542,8 @@ public class NotificationHelper {
switch (notification.getType()) { switch (notification.getType()) {
case MENTION: case MENTION:
return CHANNEL_MENTION + account.getIdentifier(); return CHANNEL_MENTION + account.getIdentifier();
case STATUS:
return CHANNEL_SUBSCRIPTIONS + account.getIdentifier();
case FOLLOW: case FOLLOW:
return CHANNEL_FOLLOW + account.getIdentifier(); return CHANNEL_FOLLOW + account.getIdentifier();
case FOLLOW_REQUEST: case FOLLOW_REQUEST:
@ -606,6 +614,9 @@ public class NotificationHelper {
case MENTION: case MENTION:
return String.format(context.getString(R.string.notification_mention_format), return String.format(context.getString(R.string.notification_mention_format),
accountName); accountName);
case STATUS:
return String.format(context.getString(R.string.notification_subscription_format),
accountName);
case FOLLOW: case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), return String.format(context.getString(R.string.notification_follow_format),
accountName); accountName);
@ -636,6 +647,7 @@ public class NotificationHelper {
case MENTION: case MENTION:
case FAVOURITE: case FAVOURITE:
case REBLOG: case REBLOG:
case STATUS:
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
return notification.getStatus().getSpoilerText(); return notification.getStatus().getSpoilerText();
} else { } else {

View File

@ -111,6 +111,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true true
} }
} }
switchPreference {
setTitle(R.string.pref_title_notification_filter_subscriptions)
key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsSubscriptions
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsSubscriptions = newValue as Boolean }
true
}
}
} }
preferenceCategory(R.string.pref_title_notification_alerts) { category -> preferenceCategory(R.string.pref_title_notification_alerts) { category ->

View File

@ -20,12 +20,16 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.settings.* import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.serialize
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -38,6 +42,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var okhttpclient: OkHttpClient lateinit var okhttpclient: OkHttpClient
@Inject
lateinit var accountManager: AccountManager
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null private var httpProxyPref: Preference? = null
@ -225,6 +232,45 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
} }
} }
preferenceCategory(R.string.pref_title_wellbeing_mode) {
switchPreference {
title = getString(R.string.limit_notifications)
setDefaultValue(false)
key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
setOnPreferenceChangeListener { _, value ->
for (account in accountManager.accounts) {
val notificationFilter = deserialize(account.notificationsFilter).toMutableSet()
if (value == true) {
notificationFilter.add(Notification.Type.FAVOURITE)
notificationFilter.add(Notification.Type.FOLLOW)
notificationFilter.add(Notification.Type.REBLOG)
} else {
notificationFilter.remove(Notification.Type.FAVOURITE)
notificationFilter.remove(Notification.Type.FOLLOW)
notificationFilter.remove(Notification.Type.REBLOG)
}
account.notificationsFilter = serialize(notificationFilter)
accountManager.saveAccount(account)
}
true
}
}
switchPreference {
title = getString(R.string.wellbeing_hide_stats_posts)
setDefaultValue(false)
key = PrefKeys.WELLBEING_HIDE_STATS_POSTS
}
switchPreference {
title = getString(R.string.wellbeing_hide_stats_profile)
setDefaultValue(false)
key = PrefKeys.WELLBEING_HIDE_STATS_PROFILE
}
}
preferenceCategory(R.string.pref_title_proxy_settings) { preferenceCategory(R.string.pref_title_proxy_settings) {
httpProxyPref = preference { httpProxyPref = preference {
setTitle(R.string.pref_title_http_proxy_settings) setTitle(R.string.pref_title_http_proxy_settings)

View File

@ -15,13 +15,10 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
@ -33,19 +30,12 @@ import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.fragment_report_done.* import kotlinx.android.synthetic.main.fragment_report_done.*
import javax.inject.Inject import javax.inject.Inject
class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
class ReportDoneFragment : Fragment(), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_done, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)

View File

@ -16,12 +16,10 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
@ -33,18 +31,12 @@ import kotlinx.android.synthetic.main.fragment_report_note.*
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class ReportNoteFragment : Fragment(), Injectable { class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_note, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fillViews() fillViews()

View File

@ -16,13 +16,11 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -42,6 +40,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
@ -50,7 +49,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.* import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -58,10 +57,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
@Inject @Inject
lateinit var accountManager: AccountManager lateinit var accountManager: AccountManager
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private lateinit var adapter: StatusesAdapter private lateinit var adapter: StatusesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -89,12 +87,6 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_statuses, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
handleClicks() handleClicks()
initStatusesView() initStatusesView()
@ -120,6 +112,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID quoteEnabled = accountManager.activeAccount?.domain in CAN_USE_QUOTE_ID
) )
@ -127,8 +120,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
viewModel.statusViewState, this) viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

View File

@ -1,11 +1,9 @@
package com.keylesspalace.tusky.components.search.fragments package com.keylesspalace.tusky.components.search.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
@ -26,13 +24,13 @@ import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import javax.inject.Inject import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(), abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
protected val viewModel: SearchViewModel by viewModels({ requireActivity() }) { viewModelFactory } protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -43,12 +41,7 @@ abstract class SearchFragment<T> : Fragment(),
abstract val data: LiveData<PagedList<T>> abstract val data: LiveData<PagedList<T>>
protected lateinit var adapter: PagedListAdapter<T, *> protected lateinit var adapter: PagedListAdapter<T, *>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_search, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter() initAdapter()
setupSwipeRefreshLayout() setupSwipeRefreshLayout()
subscribeObservables() subscribeObservables()

View File

@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -70,6 +71,7 @@ class SearchNotestockFragment : SearchFragment<Pair<Status, StatusViewData.Concr
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", false), confirmReblogs = preferences.getBoolean("confirmReblogs", false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
quoteEnabled = viewModel.quoteEnabled quoteEnabled = viewModel.quoteEnabled
) )

View File

@ -52,6 +52,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -85,6 +86,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
quoteEnabled = viewModel.quoteEnabled quoteEnabled = viewModel.quoteEnabled
) )

View File

@ -43,6 +43,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var notificationsReblogged: Boolean = true, var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true, var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true, var notificationsPolls: Boolean = true,
var notificationsSubscriptions: Boolean = true,
var notificationSound: Boolean = true, var notificationSound: Boolean = true,
var notificationVibration: Boolean = true, var notificationVibration: Boolean = true,
var notificationLight: Boolean = true, var notificationLight: Boolean = true,

View File

@ -35,7 +35,8 @@ class AccountManager @Inject constructor(db: AppDatabase) {
@Volatile @Volatile
var activeAccount: AccountEntity? = null var activeAccount: AccountEntity? = null
private var accounts: MutableList<AccountEntity> = mutableListOf() var accounts: MutableList<AccountEntity> = mutableListOf()
private set
private val accountDao: AccountDao = db.accountDao() private val accountDao: AccountDao = db.accountDao()
init { init {

View File

@ -15,14 +15,14 @@
package com.keylesspalace.tusky.db; package com.keylesspalace.tusky.db;
import com.keylesspalace.tusky.TabDataKt; import androidx.annotation.NonNull;
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.room.Database; import androidx.room.Database;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
import androidx.room.migration.Migration; import androidx.room.migration.Migration;
import androidx.annotation.NonNull; import androidx.sqlite.db.SupportSQLiteDatabase;
import com.keylesspalace.tusky.TabDataKt;
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
/** /**
* DB version & declare DAO * DB version & declare DAO
@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 23) }, version = 24)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); public abstract TootDao tootDao();
@ -339,5 +339,12 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER"); database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER");
} }
}; };
public static final Migration MIGRATION_23_24 = new Migration(23, 24) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1");
}
};
} }

View File

@ -80,7 +80,7 @@ class AppModule {
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23) AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24)
.build() .build()
} }
@ -88,4 +88,4 @@ class AppModule {
@Singleton @Singleton
fun notifier(context: Context): Notifier = SystemNotifier(context) fun notifier(context: Context): Notifier = SystemNotifier(context)
} }

View File

@ -15,7 +15,10 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import com.google.gson.* import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
data class Notification( data class Notification(
@ -32,7 +35,8 @@ data class Notification(
FAVOURITE("favourite"), FAVOURITE("favourite"),
FOLLOW("follow"), FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"), FOLLOW_REQUEST("follow_request"),
POLL("poll"); POLL("poll"),
STATUS("status");
companion object { companion object {
@ -44,7 +48,7 @@ data class Notification(
} }
return UNKNOWN return UNKNOWN
} }
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL) val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS)
} }
override fun toString(): String { override fun toString(): String {
@ -72,4 +76,14 @@ data class Notification(
} }
} }
// for Pleroma compatibility that uses Mention type
fun rewriteToStatusTypeIfNeeded(accountId: String) : Notification {
if (type == Type.MENTION && status != null) {
return if (status.mentions.any {
it.id == accountId
}) this else copy(type = Type.STATUS)
}
return this
}
} }

View File

@ -26,6 +26,8 @@ data class Relationship (
@SerializedName("muting_notifications") val mutingNotifications: Boolean, @SerializedName("muting_notifications") val mutingNotifications: Boolean,
val requested: Boolean, val requested: Boolean,
@SerializedName("showing_reblogs") val showingReblogs: Boolean, @SerializedName("showing_reblogs") val showingReblogs: Boolean,
val subscribing: Boolean? = null, // Pleroma extension
@SerializedName("domain_blocking") val blockingDomain: Boolean, @SerializedName("domain_blocking") val blockingDomain: Boolean,
val note: String? // nullable for backward compatibility / feature detection val note: String?, // nullable for backward compatibility / feature detection
val notifying: Boolean? // since 3.3.0rc
) )

View File

@ -49,7 +49,7 @@ import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.HashMap import java.util.*
import javax.inject.Inject import javax.inject.Inject
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {

View File

@ -73,6 +73,7 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment; import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -182,7 +183,9 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public NotificationViewData apply(Either<Placeholder, Notification> input) { public NotificationViewData apply(Either<Placeholder, Notification> input) {
if (input.isRight()) { if (input.isRight()) {
Notification notification = input.asRight(); Notification notification = input.asRight()
.rewriteToStatusTypeIfNeeded(accountManager.getActiveAccount().getAccountId());
return ViewDataUtils.notificationToViewData( return ViewDataUtils.notificationToViewData(
notification, notification,
alwaysShowSensitiveMedia, alwaysShowSensitiveMedia,
@ -253,6 +256,7 @@ public class NotificationsFragment extends SFragment implements
preferences.getBoolean("useBlurhash", true), preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain())
); );
@ -779,6 +783,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_follow_request_name); return getString(R.string.notification_follow_request_name);
case POLL: case POLL:
return getString(R.string.notification_poll_name); return getString(R.string.notification_poll_name);
case STATUS:
return getString(R.string.notification_subscription_name);
default: default:
return "Unknown"; return "Unknown";
} }
@ -806,6 +812,7 @@ public class NotificationsFragment extends SFragment implements
private void loadNotificationsFilter() { private void loadNotificationsFilter() {
AccountEntity account = accountManager.getActiveAccount(); AccountEntity account = accountManager.getActiveAccount();
if (account != null) { if (account != null) {
notificationFilter.clear();
notificationFilter.addAll(NotificationTypeConverterKt.deserialize( notificationFilter.addAll(NotificationTypeConverterKt.deserialize(
account.getNotificationsFilter())); account.getNotificationsFilter()));
} }
@ -1282,6 +1289,12 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
String rawAccountNotificationFilter = accountManager.getActiveAccount().getNotificationsFilter();
Set<Notification.Type> accountNotificationFilter = NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter);
if (!notificationFilter.equals(accountNotificationFilter)) {
loadNotificationsFilter();
fullyRefreshWithProgressBar(true);
}
startUpdateTimestamp(); startUpdateTimestamp();
} }

View File

@ -81,6 +81,7 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.repository.Placeholder; import com.keylesspalace.tusky.repository.Placeholder;
import com.keylesspalace.tusky.repository.TimelineRepository; import com.keylesspalace.tusky.repository.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode; import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -90,7 +91,6 @@ import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.BackgroundMessageView;
import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.view.EndlessOnScrollListener;
@ -284,6 +284,7 @@ public class TimelineFragment extends SFragment implements
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain())
); );
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);

View File

@ -59,11 +59,11 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration; import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -129,6 +129,7 @@ public final class ViewThreadFragment extends SFragment implements
thisThreadsStatusId = getArguments().getString("id"); thisThreadsStatusId = getArguments().getString("id");
SharedPreferences preferences = SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(getActivity()); PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false), preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
@ -139,6 +140,7 @@ public final class ViewThreadFragment extends SFragment implements
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain()) Arrays.asList(ComposeViewModelKt.getCAN_USE_QUOTE_ID()).contains(accountManager.getActiveAccount().getDomain())
); );
adapter = new ThreadAdapter(statusDisplayOptions, this); adapter = new ThreadAdapter(statusDisplayOptions, this);

View File

@ -307,7 +307,8 @@ interface MastodonApi {
@POST("api/v1/accounts/{id}/follow") @POST("api/v1/accounts/{id}/follow")
fun followAccount( fun followAccount(
@Path("id") accountId: String, @Path("id") accountId: String,
@Field("reblogs") showReblogs: Boolean @Field("reblogs") showReblogs: Boolean? = null,
@Field("notify") notify: Boolean? = null
): Single<Relationship> ): Single<Relationship>
@POST("api/v1/accounts/{id}/unfollow") @POST("api/v1/accounts/{id}/unfollow")
@ -347,6 +348,16 @@ interface MastodonApi {
@Path("id") accountId: String @Path("id") accountId: String
): Single<List<IdentityProof>> ): Single<List<IdentityProof>>
@POST("api/v1/pleroma/accounts/{id}/subscribe")
fun subscribeAccount(
@Path("id") accountId: String
): Single<Relationship>
@POST("api/v1/pleroma/accounts/{id}/unsubscribe")
fun unsubscribeAccount(
@Path("id") accountId: String
): Single<Relationship>
@GET("api/v1/blocks") @GET("api/v1/blocks")
fun blocks( fun blocks(
@Query("max_id") maxId: String? @Query("max_id") maxId: String?

View File

@ -37,6 +37,9 @@ object PrefKeys {
const val LIMITED_BANDWIDTH_TIMELINE_LOADING = "limitedBandwidthTimelineLoading" const val LIMITED_BANDWIDTH_TIMELINE_LOADING = "limitedBandwidthTimelineLoading"
const val CUSTOM_TABS = "customTabs" const val CUSTOM_TABS = "customTabs"
const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"
const val WELLBEING_HIDE_STATS_POSTS = "wellbeingHideStatsPosts"
const val WELLBEING_HIDE_STATS_PROFILE = "wellbeingHideStatsProfile"
const val HTTP_PROXY_ENABLED = "httpProxyEnabled" const val HTTP_PROXY_ENABLED = "httpProxyEnabled"
const val HTTP_PROXY_SERVER = "httpProxyServer" const val HTTP_PROXY_SERVER = "httpProxyServer"
@ -62,6 +65,7 @@ object PrefKeys {
const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs"
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"

View File

@ -15,6 +15,8 @@ data class StatusDisplayOptions(
val cardViewMode: CardViewMode, val cardViewMode: CardViewMode,
@get:JvmName("confirmReblogs") @get:JvmName("confirmReblogs")
val confirmReblogs: Boolean, val confirmReblogs: Boolean,
@get:JvmName("hideStats")
val hideStats: Boolean,
@get:JvmName("quoteEnabled") @get:JvmName("quoteEnabled")
val quoteEnabled: Boolean val quoteEnabled: Boolean
) )

View File

@ -126,6 +126,16 @@ class AccountViewModel @Inject constructor(
fun unmuteAccount() { fun unmuteAccount() {
changeRelationship(RelationShipAction.UNMUTE) changeRelationship(RelationShipAction.UNMUTE)
} }
fun changeSubscribingState() {
val relationship = relationshipData.value?.data
if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */
|| relationship?.subscribing == true /* Pleroma */ ) {
changeRelationship(RelationShipAction.UNSUBSCRIBE)
} else {
changeRelationship(RelationShipAction.SUBSCRIBE)
}
}
fun blockDomain(instance: String) { fun blockDomain(instance: String) {
mastodonApi.blockDomain(instance).enqueue(object: Callback<Any> { mastodonApi.blockDomain(instance).enqueue(object: Callback<Any> {
@ -180,6 +190,7 @@ class AccountViewModel @Inject constructor(
private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null) { private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null) {
val relation = relationshipData.value?.data val relation = relationshipData.value?.data
val account = accountData.value?.data val account = accountData.value?.data
val isMastodon = relationshipData.value?.data?.notifying != null
if (relation != null && account != null) { if (relation != null && account != null) {
// optimistically post new state for faster response // optimistically post new state for faster response
@ -197,17 +208,37 @@ class AccountViewModel @Inject constructor(
RelationShipAction.UNBLOCK -> relation.copy(blocking = false) RelationShipAction.UNBLOCK -> relation.copy(blocking = false)
RelationShipAction.MUTE -> relation.copy(muting = true) RelationShipAction.MUTE -> relation.copy(muting = true)
RelationShipAction.UNMUTE -> relation.copy(muting = false) RelationShipAction.UNMUTE -> relation.copy(muting = false)
RelationShipAction.SUBSCRIBE -> {
if(isMastodon)
relation.copy(notifying = true)
else relation.copy(subscribing = true)
}
RelationShipAction.UNSUBSCRIBE -> {
if(isMastodon)
relation.copy(notifying = false)
else relation.copy(subscribing = false)
}
} }
relationshipData.postValue(Loading(newRelation)) relationshipData.postValue(Loading(newRelation))
} }
when (relationshipAction) { when (relationshipAction) {
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true) RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true)
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true)
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
RelationShipAction.SUBSCRIBE -> {
if(isMastodon)
mastodonApi.followAccount(accountId, notify = true)
else mastodonApi.subscribeAccount(accountId)
}
RelationShipAction.UNSUBSCRIBE -> {
if(isMastodon)
mastodonApi.followAccount(accountId, notify = false)
else mastodonApi.unsubscribeAccount(accountId)
}
}.subscribe( }.subscribe(
{ relationship -> { relationship ->
relationshipData.postValue(Success(relationship)) relationshipData.postValue(Success(relationship))
@ -263,7 +294,6 @@ class AccountViewModel @Inject constructor(
if (!isSelf) if (!isSelf)
obtainRelationship(isReload) obtainRelationship(isReload)
} }
} }
fun setAccountInfo(accountId: String) { fun setAccountInfo(accountId: String) {
@ -273,10 +303,10 @@ class AccountViewModel @Inject constructor(
} }
enum class RelationShipAction { enum class RelationShipAction {
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
} }
companion object { companion object {
const val TAG = "AccountViewModel" const val TAG = "AccountViewModel"
} }
} }

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0 0h24v24H0V0z" />
<path
android:fillColor="#000000"
android:pathData="M18 16v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-0.83-0.68-1.5-1.51-1.5S10.5 3.17 10.5 4v0.68C7.63 5.36 6 7.92 6 11v5l-1.3 1.29c-0.63 0.63 -0.19 1.71 0.7 1.71h13.17c0.89 0 1.34-1.08 0.71 -1.71L18 16zm-6.01 6c1.1 0 2-0.9 2-2h-4c0 1.1 0.89 2 2 2zM6.77 4.73c0.42-0.38 0.43 -1.03 0.03 -1.43-0.38-0.38-1-0.39-1.39-0.02C3.7 4.84 2.52 6.96 2.14 9.34c-0.09 0.61 0.38 1.16 1 1.16 0.48 0 0.9-0.35 0.98 -0.83 0.3 -1.94 1.26-3.67 2.65-4.94zM18.6 3.28c-0.4-0.37-1.02-0.36-1.4 0.02 -0.4 0.4 -0.38 1.04 0.03 1.42 1.38 1.27 2.35 3 2.65 4.94 0.07 0.48 0.49 0.83 0.98 0.83 0.61 0 1.09-0.55 0.99 -1.16-0.38-2.37-1.55-4.48-3.25-6.05z" />
</vector>

View File

@ -71,6 +71,26 @@
app:layout_constraintStart_toEndOf="@id/accountMuteButton" app:layout_constraintStart_toEndOf="@id/accountMuteButton"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Follow Requested" /> tools:text="Follow Requested" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountSubscribeButton"
style="@style/TuskyButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="6dp"
android:minWidth="0dp"
android:paddingStart="8dp"
android:paddingEnd="4dp"
android:scaleType="centerInside"
app:icon="@drawable/ic_notifications_24dp"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton"
app:layout_constraintEnd_toStartOf="@id/accountFollowButton"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/accountMuteButton"
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/accountMuteButton" android:id="@+id/accountMuteButton"
@ -86,7 +106,7 @@
app:icon="@drawable/ic_unmute_24dp" app:icon="@drawable/ic_unmute_24dp"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton" app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton"
app:layout_constraintEnd_toStartOf="@id/accountFollowButton" app:layout_constraintEnd_toStartOf="@id/accountSubscribeButton"
app:layout_constraintHorizontal_bias="1" app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guideAvatar" app:layout_constraintStart_toStartOf="@id/guideAvatar"

View File

@ -154,8 +154,6 @@
android:layout_marginRight="14dp" android:layout_marginRight="14dp"
android:layout_marginBottom="14dp" android:layout_marginBottom="14dp"
android:contentDescription="@string/action_view_profile" android:contentDescription="@string/action_view_profile"
android:paddingRight="12dp"
android:paddingBottom="12dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:ignore="RtlHardcoded,RtlSymmetry" tools:ignore="RtlHardcoded,RtlSymmetry"
tools:src="@drawable/avatar_default" /> tools:src="@drawable/avatar_default" />

View File

@ -3,37 +3,37 @@
<string name="error_generic">Hiba történt.</string> <string name="error_generic">Hiba történt.</string>
<string name="error_network">Hálózati hiba történt! Kérjük, ellenőrizd a kapcsolatot, és próbálja meg újra!</string> <string name="error_network">Hálózati hiba történt! Kérjük, ellenőrizd a kapcsolatot, és próbálja meg újra!</string>
<string name="error_empty">Ez nem lehet üres.</string> <string name="error_empty">Ez nem lehet üres.</string>
<string name="error_invalid_domain">Helytelen domén</string> <string name="error_invalid_domain">Helytelen domain</string>
<string name="error_failed_app_registration">Sikertelen bejelentkezés ezen a szerveren.</string> <string name="error_failed_app_registration">Sikertelen hitelesítés ezen a példányon.</string>
<string name="error_no_web_browser_found">Nem található használható böngésző.</string> <string name="error_no_web_browser_found">Nem található használható böngésző.</string>
<string name="error_authorization_unknown">Azonosítatlan engedélyezési hiba történt.</string> <string name="error_authorization_unknown">Azonosítatlan engedélyezési hiba történt.</string>
<string name="error_authorization_denied">Engedély megtagadva.</string> <string name="error_authorization_denied">Engedély megtagadva.</string>
<string name="error_retrieving_oauth_token">Bejelentkezési token megszerzése sikertelen.</string> <string name="error_retrieving_oauth_token">Bejelentkezési token megszerzése sikertelen.</string>
<string name="error_compose_character_limit">Túl hosszú a tülkölés!</string> <string name="error_compose_character_limit">Túl hosszú a tülkölés!</string>
<string name="error_image_upload_size">A fájlnak kisebbnek kell lennie, mint 8MB.</string> <string name="error_image_upload_size">A fájlnak kisebbnek kell lennie, mint 8 MB.</string>
<string name="error_video_upload_size">A video fájlnak kisebbnek kell lennie, mint 40MB.</string> <string name="error_video_upload_size">A videofájloknak kisebbnek kell lenniük, mint 40 MB.</string>
<string name="error_media_upload_type">Ilyen típusú fájlt nem lehet feltölteni.</string> <string name="error_media_upload_type">Ilyen típusú fájlt nem lehet feltölteni.</string>
<string name="error_media_upload_opening">Fájl megnyitása sikertelen.</string> <string name="error_media_upload_opening">Fájl megnyitása sikertelen.</string>
<string name="error_media_upload_permission">Média olvasási engedély szükséges.</string> <string name="error_media_upload_permission">Média olvasási engedély szükséges.</string>
<string name="error_media_download_permission">Média tárolási engedély szükséges.</string> <string name="error_media_download_permission">Média tárolási engedély szükséges.</string>
<string name="error_media_upload_image_or_video">Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez.</string> <string name="error_media_upload_image_or_video">Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez.</string>
<string name="error_media_upload_sending">Feltöltés sikertelen.</string> <string name="error_media_upload_sending">Feltöltés sikertelen.</string>
<string name="error_sender_account_gone">Nem sikerült elküldeni a tülköt..</string> <string name="error_sender_account_gone">Nem sikerült elküldeni a tülköt.</string>
<string name="title_home">Kezdőlap</string> <string name="title_home">Kezdőlap</string>
<string name="title_notifications">Értesítések</string> <string name="title_notifications">Értesítések</string>
<string name="title_public_local">Helyi</string> <string name="title_public_local">Helyi</string>
<string name="title_public_federated">Föderáció</string> <string name="title_public_federated">Föderációs</string>
<string name="title_direct_messages">Közvetlen üzenetek</string> <string name="title_direct_messages">Közvetlen üzenetek</string>
<string name="title_tab_preferences">Fülek</string> <string name="title_tab_preferences">Fülek</string>
<string name="title_view_thread">Tülk</string> <string name="title_view_thread">Tülk</string>
<string name="title_statuses">Posztok</string> <string name="title_statuses">Tülkök</string>
<string name="title_statuses_with_replies">Válaszokkal</string> <string name="title_statuses_with_replies">Válaszokkal</string>
<string name="title_statuses_pinned">Rögzített</string> <string name="title_statuses_pinned">Rögzített</string>
<string name="title_follows">Követett</string> <string name="title_follows">Követett</string>
<string name="title_followers">Követő</string> <string name="title_followers">Követő</string>
<string name="title_favourites">Kedvencek</string> <string name="title_favourites">Kedvencek</string>
<string name="title_mutes">Némított felhasználók</string> <string name="title_mutes">Némított felhasználók</string>
<string name="title_blocks">Blokkolt felhasználók</string> <string name="title_blocks">Letiltott felhasználók</string>
<string name="title_follow_requests">Követési kérelmek</string> <string name="title_follow_requests">Követési kérelmek</string>
<string name="title_edit_profile">Profilod szerkesztése</string> <string name="title_edit_profile">Profilod szerkesztése</string>
<string name="title_saved_toot">Piszkozatok</string> <string name="title_saved_toot">Piszkozatok</string>
@ -122,7 +122,7 @@
<string name="confirmation_unmuted">Felhasználó némítása feloldva</string> <string name="confirmation_unmuted">Felhasználó némítása feloldva</string>
<string name="status_sent">Elküldve!</string> <string name="status_sent">Elküldve!</string>
<string name="status_sent_long">Válasz sikeresen elküldve.</string> <string name="status_sent_long">Válasz sikeresen elküldve.</string>
<string name="hint_domain">Melyik szerver\?</string> <string name="hint_domain">Melyik példány\?</string>
<string name="hint_compose">Mi jár a fejedben\?</string> <string name="hint_compose">Mi jár a fejedben\?</string>
<string name="hint_content_warning">Tartalom figyelmeztetés</string> <string name="hint_content_warning">Tartalom figyelmeztetés</string>
<string name="hint_display_name">Megjelenítési név</string> <string name="hint_display_name">Megjelenítési név</string>
@ -134,12 +134,12 @@
<string name="label_header">Fejléc</string> <string name="label_header">Fejléc</string>
<string name="link_whats_an_instance">Mi az a szerver\?</string> <string name="link_whats_an_instance">Mi az a szerver\?</string>
<string name="login_connection">Csatlakozás…</string> <string name="login_connection">Csatlakozás…</string>
<string name="dialog_whats_an_instance">Bármely szerver címét beírhatod ide, mint mastodon.social, icosahedron.website, social.tchncs.de, és <a href="https://instances.social">mások!</a> <string name="dialog_whats_an_instance">Bármely példány címét vagy domain nevét beírhatod ide, mint a mastodon.social, az icosahedron.website, a social.tchncs.de és <a href="https://instances.social">mások!</a>
\n \n
\nHa még nincs fiókod, beírhatod a címét ide annak a szervernek amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot. \nHa még nincs fiókod, beírhatod annak a példánynak a címét, amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot.
\n
\nA szerver az a hely ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más szervereken lévő emberekkel, mintha ugyanazon az oldalon lennétek.
\n \n
\nA példány az a hely, ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más példányokon lévő emberekkel, mintha ugyanazon az oldalon lennétek.
\n
\nTöbb információt találhatsz itt: <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \nTöbb információt találhatsz itt: <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
<string name="dialog_title_finishing_media_upload">Média feltöltés befejezése</string> <string name="dialog_title_finishing_media_upload">Média feltöltés befejezése</string>
<string name="dialog_message_uploading_media">Feltöltés…</string> <string name="dialog_message_uploading_media">Feltöltés…</string>
@ -198,7 +198,7 @@
<string name="notification_follow_name">Új követők</string> <string name="notification_follow_name">Új követők</string>
<string name="notification_follow_description">Értesítések új követőkről</string> <string name="notification_follow_description">Értesítések új követőkről</string>
<string name="notification_boost_name">Megtolások</string> <string name="notification_boost_name">Megtolások</string>
<string name="notification_boost_description">Értesítések posztjaid megtolása esetén</string> <string name="notification_boost_description">Értesítések tülkjeid megtolása esetén</string>
<string name="notification_favourite_name">Kedvencek</string> <string name="notification_favourite_name">Kedvencek</string>
<string name="notification_favourite_description">Értesítések mikor tülkjeidet kedvencnek jelölik</string> <string name="notification_favourite_description">Értesítések mikor tülkjeidet kedvencnek jelölik</string>
<string name="notification_mention_format">%s megemlített téged</string> <string name="notification_mention_format">%s megemlített téged</string>
@ -236,7 +236,7 @@
<string name="action_lists">Listák</string> <string name="action_lists">Listák</string>
<string name="title_lists">Listák</string> <string name="title_lists">Listák</string>
<string name="action_remove">Törlés</string> <string name="action_remove">Törlés</string>
<string name="lock_account_label">Fiók lezárása</string> <string name="lock_account_label">Fiók zárolása</string>
<string name="compose_save_draft">Elmented a vázlatot?</string> <string name="compose_save_draft">Elmented a vázlatot?</string>
<string name="send_toot_notification_title">Tülk elküldése…</string> <string name="send_toot_notification_title">Tülk elküldése…</string>
<string name="send_toot_notification_error_title">A tülk elküldése nem sikerült</string> <string name="send_toot_notification_error_title">A tülk elküldése nem sikerült</string>
@ -433,7 +433,7 @@
<string name="action_access_scheduled_toot">Időzített tülkök</string> <string name="action_access_scheduled_toot">Időzített tülkök</string>
<string name="action_schedule_toot">Tülk Időzítése</string> <string name="action_schedule_toot">Tülk Időzítése</string>
<string name="action_reset_schedule">Visszaállítás</string> <string name="action_reset_schedule">Visszaállítás</string>
<string name="post_lookup_error_format">Nem találjuk ezt a posztot %s</string> <string name="post_lookup_error_format">Nem találjuk ezt a tülköt %s</string>
<string name="title_bookmarks">Könyvjelzők</string> <string name="title_bookmarks">Könyvjelzők</string>
<string name="action_bookmark">Könyvjelzőzés</string> <string name="action_bookmark">Könyvjelzőzés</string>
<string name="action_view_bookmarks">Könyvjelzők</string> <string name="action_view_bookmarks">Könyvjelzők</string>
@ -441,7 +441,7 @@
<string name="description_status_bookmarked">Könyvjelzőzve</string> <string name="description_status_bookmarked">Könyvjelzőzve</string>
<string name="select_list_title">Lista kiválasztása</string> <string name="select_list_title">Lista kiválasztása</string>
<string name="list">Lista</string> <string name="list">Lista</string>
<string name="error_audio_upload_size">A hangfájlok mérete 40MB-nál kisebb kell legyen.</string> <string name="error_audio_upload_size">A hangfájloknak kisebbnek kell lenniük, mint 40 MB.</string>
<string name="no_saved_status">Nincs egy vázlatod sem.</string> <string name="no_saved_status">Nincs egy vázlatod sem.</string>
<string name="no_scheduled_status">Nincs egy ütemezett tülköd sem.</string> <string name="no_scheduled_status">Nincs egy ütemezett tülköd sem.</string>
<string name="warning_scheduling_interval">A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc.</string> <string name="warning_scheduling_interval">A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc.</string>
@ -462,7 +462,7 @@
<string name="pref_title_gradient_for_media">Színes homály mutatása rejtett médiánál</string> <string name="pref_title_gradient_for_media">Színes homály mutatása rejtett médiánál</string>
<string name="pref_title_notification_filter_follow_requests">követni szeretnének</string> <string name="pref_title_notification_filter_follow_requests">követni szeretnének</string>
<string name="dialog_mute_hide_notifications">Értesítések elrejtése</string> <string name="dialog_mute_hide_notifications">Értesítések elrejtése</string>
<string name="dialog_block_warning">Letiltsuk @%s -t\?</string> <string name="dialog_block_warning">Letiltod: @%s\?</string>
<string name="dialog_mute_warning">Elnémítsuk @%s fiókot\?</string> <string name="dialog_mute_warning">Elnémítsuk @%s fiókot\?</string>
<string name="action_unmute_conversation">Beszélgetés némításának feloldása</string> <string name="action_unmute_conversation">Beszélgetés némításának feloldása</string>
<string name="action_mute_conversation">Beszélgetés némítása</string> <string name="action_mute_conversation">Beszélgetés némítása</string>

View File

@ -67,6 +67,7 @@
<string name="notification_favourite_format">%s favorited your toot</string> <string name="notification_favourite_format">%s favorited your toot</string>
<string name="notification_follow_format">%s followed you</string> <string name="notification_follow_format">%s followed you</string>
<string name="notification_follow_request_format">%s requested to follow you</string> <string name="notification_follow_request_format">%s requested to follow you</string>
<string name="notification_subscription_format">%s just posted</string>
<string name="report_username_format">Report @%s</string> <string name="report_username_format">Report @%s</string>
<string name="report_comment_hint">Additional comments?</string> <string name="report_comment_hint">Additional comments?</string>
@ -240,6 +241,7 @@
<string name="pref_title_notification_filter_reblogs">my posts are boosted</string> <string name="pref_title_notification_filter_reblogs">my posts are boosted</string>
<string name="pref_title_notification_filter_favourites">my posts are favorited</string> <string name="pref_title_notification_filter_favourites">my posts are favorited</string>
<string name="pref_title_notification_filter_poll">polls have ended</string> <string name="pref_title_notification_filter_poll">polls have ended</string>
<string name="pref_title_notification_filter_subscriptions">somebody I\'m subscribed to published a new toot</string>
<string name="pref_title_appearance_settings">Appearance</string> <string name="pref_title_appearance_settings">Appearance</string>
<string name="pref_title_app_theme">App Theme</string> <string name="pref_title_app_theme">App Theme</string>
<string name="pref_title_timelines">Timelines</string> <string name="pref_title_timelines">Timelines</string>
@ -313,7 +315,8 @@
<string name="notification_favourite_description">Notifications when your toots get marked as favorite</string> <string name="notification_favourite_description">Notifications when your toots get marked as favorite</string>
<string name="notification_poll_name">Polls</string> <string name="notification_poll_name">Polls</string>
<string name="notification_poll_description">Notifications about polls that have ended</string> <string name="notification_poll_description">Notifications about polls that have ended</string>
<string name="notification_subscription_name">New toots</string>
<string name="notification_subscription_description">Notifications when somebody you\'re subscribed to published a new toot</string>
<string name="notification_mention_format">%s mentioned you</string> <string name="notification_mention_format">%s mentioned you</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string> <string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>
@ -607,7 +610,19 @@
<string name="pref_title_show_cards_in_timelines">Show link previews in timelines</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> <string name="pref_title_confirm_reblogs">Show confirmation dialog before boosting</string>
<string name="pref_title_hide_top_toolbar">Hide the title of the top toolbar</string> <string name="pref_title_hide_top_toolbar">Hide the title of the top toolbar</string>
<string name="pref_title_wellbeing_mode">Wellbeing</string>
<string name="account_note_hint">Your private note about this account</string> <string name="account_note_hint">Your private note about this account</string>
<string name="account_note_saved">Saved!</string> <string name="account_note_saved">Saved!</string>
<string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n
- Favorite/Boost/Follow notifications\n
- Favorite/Boost count on toots\n
- Follower/Post stats on profiles\n\n
Push-notifications will not be affected, but you can review your notification preferences manually.
</string>
<string name="review_notifications">Review Notifications</string>
<string name="limit_notifications">Limit timeline notifications</string>
<string name="wellbeing_hide_stats_posts">Hide quantitative stats on posts</string>
<string name="wellbeing_hide_stats_profile">Hide quantitative stats on profiles</string>
<string name="error_upload_max_media_reached">You cannot upload more than %1$d media attachments.</string>
</resources> </resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorSurface">@color/white</color> <color name="colorSurface">@color/tusky_grey_95</color>
<color name="colorPrimaryDark">@color/tusky_grey_70</color> <color name="colorPrimaryDark">@color/tusky_grey_70</color>
<color name="colorBackground">@color/tusky_grey_95</color> <color name="colorBackground">@color/white</color>
<color name="windowBackground">@color/tusky_grey_80</color> <color name="windowBackground">@color/tusky_grey_80</color>
<color name="textColorPrimary">@color/tusky_grey_10</color> <color name="textColorPrimary">@color/tusky_grey_10</color>

View File

@ -1,10 +0,0 @@
Tusky v13.0
- support for profile notes (Mastodon 3.2.0 feature)
- support for admin announcements (Mastodon 3.1.0 feature)
- the avatar of your selected account will now be shown in the main toolbar
- clicking the display name in a timeline will now open the profile page of that user
- a lot of bug fixes and small improvements
- improved translations

View File

@ -1,10 +0,0 @@
Tusky v13.0
- privát profilmegjegyzések támogatása (Mastodon 3.2.0 funkció)
- adminisztrátori közlemények támogatása (Mastodon 3.1.0 funkció)
- az éppen használt fiókod avatarja mostantól látszik az eszköztáron
- az idővonalon egy profilra kattintva előjön a felhasználó profiloldala
- rengeteg hibajavítás és apró fejlesztés
- javított fordítások

View File

@ -1,10 +0,0 @@
Tusky útg. 13.0
- stuðningur við minnispunkta í sniðum (Mastodon 3.2.0 eiginleiki)
- stuðningur við tilkynningar frá stjórnendum (Mastodon 3.1.0 eiginleiki)
- auðkennismynd úr völdum aðgangi birist núna í aðalverkfærastikunni
- smellt á birtingarnafn á tímalínu opnar núna notandasniðssíðu þess notanda
- hellingur að villulagfæringum og minni betrumbótum
- bættar þýðingar

View File

@ -1,8 +0,0 @@
Tusky v10.0
- Ora puoi contrassegnare gli stati ed elencare i tuoi segnalibri in Tusky.
- Ora puoi programmare i tuoi toot con Tusky. Tieni presente che il tempo selezionato deve essere di almeno 5 minuti in futuro.
- Ora puoi aggiungere elenchi alla schermata principale.
- Ora puoi pubblicare allegati audio con Tusky.
E molti altri piccoli miglioramenti e correzioni di bug!

View File

@ -1,8 +0,0 @@
Tusky v12.0
- Interfaccia principale migliorata - ora puoi spostare le schede in basso
- Quando si disattiva l'audio di un utente, ora è possibile anche decidere se disattivare l'audio delle sue notifiche
- Ora puoi seguire tutti gli hashtag che desideri in una singola scheda hashtag
- Migliorata la modalità di visualizzazione delle descrizioni dei media in modo che funzioni anche per descrizioni molto lunghe
Log delle modifiche completo: https://github.com/tuskyapp/Tusky/releases

View File

@ -1,10 +0,0 @@
Tusky v13.0
- supporto per le note del profilo (funzionalità di Mastodon 3.2.0)
- supporto per gli annunci dell'amministratore (funzionalità di Mastodon 3.1.0)
- l'avatar del tuo account selezionato verrà ora mostrato nella barra degli strumenti principale
- facendo clic sul nome visualizzato in una sequenza temporale si aprirà ora la pagina del profilo di quell'utente
- molte correzioni di bug e piccoli miglioramenti
- traduzioni migliorate

View File

@ -1,10 +0,0 @@
Tusky v13.0
- støtte for profilnotater (Mastodon 3.2.0-funksjonalitet)
- støtte for administratorkunngjøringer (Mastodon 3.1.0-funksjonalitet)
- avataren som tilhører valgt konto vil nå vises på hovedverktøylinjen
- trykk på en brukers visningsnavn i tidslinjen vil åpne profilen til brukeren
- mange feilrettinger og mindre forbedringer
- forbedrede oversettelser

View File

@ -1,8 +0,0 @@
Tusky v12.0
- Improved main interface - you can now move the tabs to the bottom
- When muting a user, you can now also decide whether to mute their notifications
- You can now follow as many hashtags as you want in one single hashtag tab
- Improved the way media descriptions are displayed so it works even for super long descriptions
Full changelog: https://github.com/tuskyapp/Tusky/releases

View File

@ -1,10 +0,0 @@
Tusky v13.0
- support for profile notes (Mastodon 3.2.0 feature)
- support for admin announcements (Mastodon 3.1.0 feature)
- the avatar of your selected account will now be shown in the main toolbar
- clicking the display name in a timeline will now open the profile page of that user
- a lot of bug fixes and small improvements
- improved translations

View File

@ -1,8 +0,0 @@
Tusky v13.0
- Hỗ trợ ghi chú về một ai đó (tính năng Mastodon 3.2.0)
- Hỗ trợ hiện thông báo máy chủ (tính năng Mastodon 3.1.0)
- Ảnh đại diện của tài khoản từ giờ sẽ hiện trên thanh menu chính
- Nhấn vào tên ai đó trên bảng tin sẽ chuyển tới trang cá nhân của họ
- Sửa lỗi linh tinh và cải thiện hiệu năng
- Trau dồi bản dịch

View File

@ -1,10 +0,0 @@
Tusky v13.0
- 支持账号备注Mastodon 3.2.0 特性)
- 支持公告栏Mastodon 3.1.0特性)
- 当前账号的头像将在导航栏显示
- 在时间线中点击账号名称后打开该用户的资料页
- 其他许多小改进和错误修复
- 改善翻译

View File

@ -1 +0,0 @@
Tusky