Merge branch 'develop' into notification_policy

This commit is contained in:
Conny Duck 2024-11-05 21:07:47 +01:00
commit 954046e647
No known key found for this signature in database
16 changed files with 1503 additions and 141 deletions

View File

@ -28,6 +28,7 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.FilterUpdatedEvent
import com.keylesspalace.tusky.components.filters.EditFilterActivity
import com.keylesspalace.tusky.components.filters.FilterExpiration
import com.keylesspalace.tusky.components.filters.FiltersActivity
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
@ -251,7 +252,7 @@ class StatusListActivity : BottomSheetActivity() {
title = "#$tag",
context = listOf(Filter.Kind.HOME.kind),
filterAction = Filter.Action.WARN.action,
expiresInSeconds = null
expiresIn = FilterExpiration.never
).fold(
{ filter ->
if (mastodonApi.addFilterKeyword(
@ -281,7 +282,7 @@ class StatusListActivity : BottomSheetActivity() {
listOf(Filter.Kind.HOME.kind),
irreversible = false,
wholeWord = true,
expiresInSeconds = null
expiresIn = FilterExpiration.never
).fold(
{ filter ->
mutedFilterV1 = filter
@ -358,7 +359,7 @@ class StatusListActivity : BottomSheetActivity() {
context = filter.context.filter { it != Filter.Kind.HOME.kind },
irreversible = null,
wholeWord = null,
expiresInSeconds = null
expiresIn = FilterExpiration.never
)
} else {
mastodonApi.deleteFilterV1(filter.id)

View File

@ -1186,12 +1186,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext())
.load(card.getImage())
.dontTransform();
.load(card.getImage());
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
}
builder.into(cardImage);
builder.centerInside()
.into(cardImage);
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
int radius = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_radius);
@ -1213,7 +1213,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Glide.with(cardImage.getContext())
.load(decodeBlurHash(card.getBlurhash()))
.dontTransform()
.into(cardImage);
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);

View File

@ -1,6 +1,20 @@
/* Copyright 2024 Tusky contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.filters
import android.content.Context
import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle
import android.view.WindowManager
@ -28,7 +42,6 @@ import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import dagger.hilt.android.AndroidEntryPoint
import java.util.Date
import javax.inject.Inject
import kotlinx.coroutines.launch
@ -123,6 +136,7 @@ class EditFilterActivity : BaseActivity() {
if (originalFilter == null) {
binding.filterActionWarn.isChecked = true
initializeDurationDropDown(false)
} else {
loadFilter()
}
@ -164,7 +178,11 @@ class EditFilterActivity : BaseActivity() {
// Populate the UI from the filter's members
private fun loadFilter() {
viewModel.load(filter)
val durationNames = if (filter.expiresAt != null) {
initializeDurationDropDown(withNoChange = filter.expiresAt != null)
}
private fun initializeDurationDropDown(withNoChange: Boolean) {
val durationNames = if (withNoChange) {
arrayOf(getString(R.string.duration_no_change)) + resources.getStringArray(R.array.filter_duration_names)
} else {
resources.getStringArray(R.array.filter_duration_names)
@ -321,19 +339,5 @@ class EditFilterActivity : BaseActivity() {
companion object {
const val FILTER_TO_EDIT = "FilterToEdit"
// Mastodon *stores* the absolute date in the filter,
// but create/edit take a number of seconds (relative to the time the operation is posted)
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? {
return when (index) {
-1 -> if (default == null) {
default
} else {
((default.time - System.currentTimeMillis()) / 1000).toInt()
}
0 -> null
else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index)
}
}
}
}

View File

@ -1,9 +1,25 @@
/* Copyright 2024 Tusky contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.filters
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterKeyword
import com.keylesspalace.tusky.network.MastodonApi
@ -112,12 +128,12 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
durationIndex: Int,
context: Context
): Boolean {
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
val expiration = getExpirationForDurationIndex(durationIndex, context)
api.createFilter(
title = title,
context = contexts,
filterAction = action,
expiresInSeconds = expiresInSeconds
expiresIn = expiration
).fold(
{ newFilter ->
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work
@ -133,7 +149,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
return (
throwable.isHttpNotFound() &&
// Endpoint not found, fall back to v1 api
createFilterV1(contexts, expiresInSeconds)
createFilterV1(contexts, expiration)
)
}
)
@ -147,13 +163,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
durationIndex: Int,
context: Context
): Boolean {
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
val expiration = getExpirationForDurationIndex(durationIndex, context)
api.updateFilter(
id = originalFilter.id,
title = title,
context = contexts,
filterAction = action,
expiresInSeconds = expiresInSeconds
expires = expiration
).fold(
{
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work
@ -173,7 +189,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
{ throwable ->
if (throwable.isHttpNotFound()) {
// Endpoint not found, fall back to v1 api
if (updateFilterV1(contexts, expiresInSeconds)) {
if (updateFilterV1(contexts, expiration)) {
return true
}
}
@ -182,13 +198,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
)
}
private suspend fun createFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
private suspend fun createFilterV1(context: List<String>, expiration: FilterExpiration?): Boolean {
return _keywords.value.map { keyword ->
api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds)
api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiration)
}.none { it.isFailure }
}
private suspend fun updateFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
private suspend fun updateFilterV1(context: List<String>, expiration: FilterExpiration?): Boolean {
val results = _keywords.value.map { keyword ->
if (originalFilter == null) {
api.createFilterV1(
@ -196,7 +212,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
context = context,
irreversible = false,
wholeWord = keyword.wholeWord,
expiresInSeconds = expiresInSeconds
expiresIn = expiration
)
} else {
api.updateFilterV1(
@ -205,7 +221,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
context = context,
irreversible = false,
wholeWord = keyword.wholeWord,
expiresInSeconds = expiresInSeconds
expiresIn = expiration
)
}
}
@ -213,4 +229,18 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel(
return results.none { it.isFailure }
}
companion object {
// Mastodon *stores* the absolute date in the filter,
// but create/edit take a number of seconds (relative to the time the operation is posted)
private fun getExpirationForDurationIndex(index: Int, context: Context): FilterExpiration? {
return when (index) {
-1 -> FilterExpiration.unchanged
0 -> FilterExpiration.never
else -> FilterExpiration.seconds(
context.resources.getIntArray(R.array.filter_duration_values)[index]
)
}
}
}
}

View File

@ -0,0 +1,37 @@
/* Copyright 2024 Tusky contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.filters
import kotlin.jvm.JvmInline
/**
* Custom class to have typesafety for filter expirations.
* Retrofit will call toString when sending this class as part of a form-urlencoded body.
*/
@JvmInline
value class FilterExpiration private constructor(val seconds: Int) {
override fun toString(): String {
return if (seconds < 0) "" else seconds.toString()
}
companion object {
val unchanged: FilterExpiration? = null
val never: FilterExpiration = FilterExpiration(-1)
fun seconds(seconds: Int): FilterExpiration = FilterExpiration(seconds)
}
}

View File

@ -153,6 +153,9 @@ class NotificationsViewModel @Inject constructor(
return when ((notificationViewData as? NotificationViewData.Concrete)?.type) {
Notification.Type.MENTION, Notification.Type.POLL -> {
notificationViewData.statusViewData?.let { statusViewData ->
if (statusViewData.status.account.id == account.accountId) {
return Filter.Action.NONE
}
statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable)
return statusViewData.filterAction
}

View File

@ -76,8 +76,6 @@ class CachedTimelineViewModel @Inject constructor(
filterModel
) {
private val account = accountManager.activeAccount!!
private var currentPagingSource: PagingSource<Int, HomeTimelineData>? = null
/** Map from status id to translation. */

View File

@ -304,7 +304,7 @@ class NetworkTimelineViewModel @Inject constructor(
}
override fun clearWarning(status: StatusViewData.Concrete) {
updateStatusByActionableId(status.id) {
updateStatusByActionableId(status.actionableId) {
it.copy(filtered = emptyList())
}
}

View File

@ -46,6 +46,8 @@ abstract class TimelineViewModel(
private val filterModel: FilterModel
) : ViewModel() {
protected val account = accountManager.activeAccount!!
abstract val statuses: Flow<PagingData<StatusViewData>>
var kind: Kind = Kind.HOME
@ -179,6 +181,10 @@ abstract class TimelineViewModel(
protected fun shouldFilterStatus(statusViewData: StatusViewData): Filter.Action {
val status = statusViewData.asStatusOrNull()?.status ?: return Filter.Action.NONE
if (status.actionableStatus.account.id == account.accountId) {
// never filter own posts
return Filter.Action.NONE
}
return if (
(status.inReplyToId != null && filterRemoveReplies) ||
(status.reblog != null && filterRemoveReblogs) ||

View File

@ -68,6 +68,8 @@ class ViewThreadViewModel @Inject constructor(
private val moshi: Moshi
) : ViewModel() {
private val activeAccount = accountManager.activeAccount!!
private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState)
val uiState: Flow<ThreadUiState> = _uiState.asStateFlow()
@ -80,14 +82,10 @@ class ViewThreadViewModel @Inject constructor(
var isInitialLoad: Boolean = true
private val alwaysShowSensitiveMedia: Boolean
private val alwaysOpenSpoiler: Boolean
private val alwaysShowSensitiveMedia: Boolean = activeAccount.alwaysShowSensitiveMedia
private val alwaysOpenSpoiler: Boolean = activeAccount.alwaysOpenSpoiler
init {
val activeAccount = accountManager.activeAccount
alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
viewModelScope.launch {
eventHub.events
.collect { event ->
@ -109,7 +107,7 @@ class ViewThreadViewModel @Inject constructor(
val filterCall = async { filterModel.init(Filter.Kind.THREAD) }
val contextCall = async { api.statusContext(id) }
val statusAndAccount = db.timelineStatusDao().getStatusWithAccount(accountManager.activeAccount!!.id, id)
val statusAndAccount = db.timelineStatusDao().getStatusWithAccount(activeAccount.id, id)
var detailedStatus = if (statusAndAccount != null) {
Log.d(TAG, "Loaded status from local timeline")
@ -142,7 +140,7 @@ class ViewThreadViewModel @Inject constructor(
if (statusAndAccount != null) {
api.status(id).onSuccess { result ->
db.timelineStatusDao().update(
tuskyAccountId = accountManager.activeAccount!!.id,
tuskyAccountId = activeAccount.id,
status = result,
moshi = moshi
)
@ -421,7 +419,7 @@ class ViewThreadViewModel @Inject constructor(
private fun List<StatusViewData.Concrete>.filter(): List<StatusViewData.Concrete> {
return filter { status ->
if (status.isDetailed) {
if (status.isDetailed || status.status.account.id == activeAccount.accountId) {
true
} else {
status.filterAction = filterModel.shouldFilterStatus(status.status)

View File

@ -180,9 +180,10 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo
protected fun more(status: Status, view: View, position: Int, translation: Translation?) {
val id = status.actionableId
val accountId = status.actionableStatus.account.id
val accountUsername = status.actionableStatus.account.username
val statusUrl = status.actionableStatus.url
val actionableStatus = status.actionableStatus
val accountId = actionableStatus.account.id
val accountUsername = actionableStatus.account.username
val statusUrl = actionableStatus.url
var loggedInAccountId: String? = null
val activeAccount = accountManager.activeAccount
if (activeAccount != null) {
@ -194,22 +195,21 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo
if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more_for_user)
val menu = popup.menu
when (status.visibility) {
when (actionableStatus.visibility) {
Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
menu.add(
0,
R.id.pin,
1,
getString(
if (status.pinned) R.string.unpin_action else R.string.pin_action
if (actionableStatus.pinned) R.string.unpin_action else R.string.pin_action
)
)
}
Status.Visibility.PRIVATE -> {
val reblogged = status.reblog?.reblogged ?: status.reblogged
menu.findItem(R.id.status_reblog_private).isVisible = !reblogged
menu.findItem(R.id.status_unreblog_private).isVisible = reblogged
menu.findItem(R.id.status_reblog_private).isVisible = !actionableStatus.reblogged
menu.findItem(R.id.status_unreblog_private).isVisible = actionableStatus.reblogged
}
else -> {}
@ -298,7 +298,7 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo
}
R.id.status_download_media -> {
requestDownloadAllMedia(status)
requestDownloadAllMedia(actionableStatus)
return@setOnMenuItemClickListener true
}
@ -344,7 +344,7 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo
R.id.pin -> {
viewLifecycleOwner.lifecycleScope.launch {
timelineCases.pin(status.id, !status.pinned)
timelineCases.pin(status.actionableId, !actionableStatus.pinned)
.onFailure { e: Throwable ->
val message = e.message
?: getString(if (status.pinned) R.string.failed_to_unpin else R.string.failed_to_pin)

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.network
import at.connyduck.calladapter.networkresult.NetworkResult
import com.keylesspalace.tusky.components.filters.FilterExpiration
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Announcement
@ -543,7 +544,7 @@ interface MastodonApi {
@Field("context[]") context: List<String>,
@Field("irreversible") irreversible: Boolean?,
@Field("whole_word") wholeWord: Boolean?,
@Field("expires_in") expiresInSeconds: Int?
@Field("expires_in") expiresIn: FilterExpiration?
): NetworkResult<FilterV1>
@FormUrlEncoded
@ -554,7 +555,7 @@ interface MastodonApi {
@Field("context[]") context: List<String>,
@Field("irreversible") irreversible: Boolean?,
@Field("whole_word") wholeWord: Boolean?,
@Field("expires_in") expiresInSeconds: Int?
@Field("expires_in") expiresIn: FilterExpiration?
): NetworkResult<FilterV1>
@DELETE("api/v1/filters/{id}")
@ -566,7 +567,7 @@ interface MastodonApi {
@Field("title") title: String,
@Field("context[]") context: List<String>,
@Field("filter_action") filterAction: String,
@Field("expires_in") expiresInSeconds: Int?
@Field("expires_in") expiresIn: FilterExpiration?
): NetworkResult<Filter>
@FormUrlEncoded
@ -576,7 +577,7 @@ interface MastodonApi {
@Field("title") title: String? = null,
@Field("context[]") context: List<String>? = null,
@Field("filter_action") filterAction: String? = null,
@Field("expires_in") expiresInSeconds: Int? = null
@Field("expires_in") expires: FilterExpiration? = null
): NetworkResult<Filter>
@DELETE("api/v2/filters/{id}")

View File

@ -52,11 +52,11 @@
<string name="button_done">Xong</string>
<string name="action_send_public">ĐĂNG</string>
<string name="button_back">Quay lại</string>
<string name="button_continue">Tiếp tục</string>
<string name="button_continue">TIẾP TỤC</string>
<string name="filter_dialog_update_button">Cập nhật</string>
<string name="filter_dialog_remove_button">Xóa</string>
<string name="action_send">ĐĂNG</string>
<string name="action_login">Đăng nhập Mastodon</string>
<string name="action_login">ĐĂNG NHẬP MASTODON</string>
<string name="dialog_redraft_post_warning">Xóa và viết lại tút này\?</string>
<string name="dialog_delete_post_warning">Xóa tút này\?</string>
<string name="dialog_unfollow_warning">Bỏ theo dõi người này\?</string>
@ -90,7 +90,7 @@
<string name="send_post_link_to">Đăng lại URL tút với…</string>
<string name="send_post_content_to">Đăng lại tút với…</string>
<string name="downloading_media">Đang lưu vào thiết bị</string>
<string name="download_media">Tải xuống</string>
<string name="download_media">Lưu media</string>
<string name="action_share_as">Đăng lại với tư cách …</string>
<string name="action_open_as">Mở với tư cách %1$s</string>
<string name="action_copy_link">Chép URL</string>
@ -98,7 +98,7 @@
<string name="action_open_media_n">Mở tập tin #%1$d</string>
<string name="title_links_dialog">Links</string>
<string name="title_mentions_dialog">Lượt nhắc tới</string>
<string name="title_hashtags_dialog">Hashtag</string>
<string name="title_hashtags_dialog">HASHTAG</string>
<string name="action_open_faved_by">Xem lượt thích</string>
<string name="action_open_reblogged_by">Xem lượt đăng lại</string>
<string name="action_open_reblogger">Xem lượt đăng lại</string>
@ -139,7 +139,7 @@
<string name="action_view_favourites">Thích</string>
<string name="action_view_profile">Trang hồ sơ</string>
<string name="action_close">Đóng</string>
<string name="action_retry">Thử lại</string>
<string name="action_retry">THỬ LẠI</string>
<string name="action_delete_and_redraft">Xóa &amp; viết lại</string>
<string name="action_delete">Xóa</string>
<string name="action_edit">Sửa</string>
@ -148,8 +148,8 @@
<string name="action_hide_reblogs">Ẩn lượt đăng lại</string>
<string name="action_unblock">Bỏ chặn</string>
<string name="action_block">Chặn</string>
<string name="action_unfollow">Bỏ theo dõi</string>
<string name="action_follow">Theo dõi</string>
<string name="action_unfollow">BỎ THEO DÕI</string>
<string name="action_follow">THEO DÕI</string>
<string name="action_logout_confirm">Bạn có muốn đăng xuất tài khoản %1$s\? Dữ liệu của tài khoản sẽ bị xóa, bao gồm những bản nháp và thiết lập.</string>
<string name="action_compose">Soạn tút</string>
<string name="action_more">Thêm</string>
@ -183,36 +183,36 @@
<string name="title_bookmarks">Những tút đã lưu</string>
<string name="title_followers">Người theo dõi</string>
<string name="title_follows">Theo dõi</string>
<string name="title_posts_pinned">Ghim</string>
<string name="title_posts_with_replies">Lượt trả lời</string>
<string name="title_posts">Tút</string>
<string name="title_view_thread">Nội dung tút</string>
<string name="title_posts_pinned">GHIM</string>
<string name="title_posts_with_replies">LƯỢT TRẢ LỜI</string>
<string name="title_posts">TÚT</string>
<string name="title_view_thread">Tút</string>
<string name="title_tab_preferences">Xếp tab</string>
<string name="title_direct_messages">Nhắn riêng</string>
<string name="title_public_federated">Liên hợp</string>
<string name="title_public_local">Máy chủ</string>
<string name="title_notifications">Thông báo</string>
<string name="title_home">Trang chính</string>
<string name="title_home">Trang ch</string>
<string name="title_drafts">Những tút nháp</string>
<string name="title_favourites">Những tút đã thích</string>
<string name="link_whats_an_instance">Máy chủ là gì\?</string>
<string name="pref_title_show_media_preview">Hiện xem trước hình ảnh</string>
<string name="pref_title_show_replies">Hiện những trả lời</string>
<string name="pref_title_show_boosts">Hiện lượt đăng lại</string>
<string name="pref_title_show_replies">Hiện những tút dạng trả lời</string>
<string name="pref_title_show_boosts">Hiện những lượt đăng lại</string>
<string name="pref_title_post_tabs">Trang chủ</string>
<string name="pref_title_post_filter">Lọc bảng tin</string>
<string name="pref_title_gradient_for_media">Phủ màu media nhạy cảm</string>
<string name="pref_title_gradient_for_media">Làm mờ media nhạy cảm</string>
<string name="pref_title_animate_gif_avatars">Ảnh đại diện GIF</string>
<string name="pref_title_bot_overlay">Icon cho tài khoản Bot</string>
<string name="pref_title_language">Ngôn ngữ</string>
<string name="pref_title_custom_tabs">Mở luôn trong app</string>
<string name="pref_title_browser_settings">Trình duyệt</string>
<string name="app_theme_system">Mặc định của thiết bị</string>
<string name="app_theme_system">Giống thiết bị</string>
<string name="app_theme_auto">Tự động khi trời tối</string>
<string name="app_theme_black">Đen</string>
<string name="app_theme_light">Sáng</string>
<string name="app_them_dark">Tối</string>
<string name="pref_title_timeline_filters">Bộ lọc</string>
<string name="pref_title_timeline_filters">Những từ khóa đã lọc</string>
<string name="pref_title_timelines">Bảng tin</string>
<string name="pref_title_app_theme">Chủ đề</string>
<string name="pref_title_appearance_settings">Giao diện</string>
@ -254,9 +254,9 @@
<string name="pref_main_nav_position_option_top">Trên màn hình</string>
<string name="pref_main_nav_position">Vị trí menu</string>
<string name="pref_failed_to_sync">Không thể lưu thiết lập</string>
<string name="pref_publishing">Đăng mặc định</string>
<string name="pref_default_media_sensitivity">Tài khoản nhạy cảm (đồng bộ máy chủ)</string>
<string name="pref_default_post_privacy">Kiểu tút mặc định (đồng bộ máy chủ)</string>
<string name="pref_publishing">Mặc định khi đăng</string>
<string name="pref_default_media_sensitivity">Media nhạy cảm (đ.bộ máy chủ)</string>
<string name="pref_default_post_privacy">Kiểu tút (đ.bộ máy chủ)</string>
<string name="pref_title_http_proxy_server">Máy chủ proxy</string>
<string name="pref_title_http_proxy_port">Cổng</string>
<string name="pref_title_http_proxy_enable">Bật proxy</string>
@ -268,7 +268,7 @@
<string name="about_bug_feature_request_site">Báo lỗi và đề xuất tính năng
\nhttps://github.com/tuskyapp/Tusky/issues</string>
<string name="about_project_site">Website dự án: https://tusky.app</string>
<string name="about_tusky_license">Tusky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Bạn có thể tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_tusky_license">Tusky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="about_powered_by_tusky">Powered by Tusky</string>
<string name="about_tusky_version">Tusky %1$s</string>
<string name="description_account_locked">Tài khoản bị khóa</string>
@ -288,13 +288,13 @@
<string name="pref_title_alway_show_sensitive_media">Hiện nội dung nhạy cảm</string>
<string name="follows_you">Đang theo dõi bạn</string>
<string name="abbreviated_seconds_ago">%1$ds</string>
<string name="abbreviated_minutes_ago">%1$d phút</string>
<string name="abbreviated_hours_ago">%1$d giờ</string>
<string name="abbreviated_minutes_ago">%1$dp</string>
<string name="abbreviated_hours_ago">%1$dh</string>
<string name="abbreviated_days_ago">%1$d ngày</string>
<string name="abbreviated_years_ago">%1$d năm</string>
<string name="abbreviated_in_seconds">%1$ds</string>
<string name="abbreviated_in_minutes">%1$d phút</string>
<string name="abbreviated_in_hours">%1$d giờ</string>
<string name="abbreviated_in_hours">%1$dh%1$ giờ</string>
<string name="abbreviated_in_days">%1$d ngày</string>
<string name="abbreviated_in_years">%1$d năm</string>
<string name="state_follow_requested">Yêu cầu theo dõi</string>
@ -322,7 +322,7 @@
<string name="create_poll_title">Bình chọn</string>
<string name="pref_title_enable_swipe_for_tabs">Vuốt qua lại giữa các tab</string>
<string name="failed_search">Không thể tìm thấy</string>
<string name="title_accounts">Người</string>
<string name="title_accounts">NGƯỜI</string>
<string name="report_description_remote_instance">Người này thuộc máy chủ khác. Gửi luôn cho máy chủ đó\?</string>
<string name="report_description_1">Báo cáo này sẽ được gửi tới kiểm duyệt viên. Hãy cho biết lý do vì sao bạn báo cáo người này bên dưới:</string>
<string name="failed_fetch_posts">Chưa tải được tút</string>
@ -358,8 +358,8 @@
<string name="compose_shortcut_short_label">Soạn</string>
<string name="compose_shortcut_long_label">Soạn tút</string>
<string name="filter_apply">Áp dụng</string>
<string name="notifications_apply_filter">Lọc</string>
<string name="notifications_clear">Xóa hết</string>
<string name="notifications_apply_filter">LỌC</string>
<string name="notifications_clear">XÓA HẾT</string>
<string name="list">Danh sách</string>
<string name="select_list_title">Chọn danh sách</string>
<string name="hashtags">Hashtag</string>
@ -379,13 +379,13 @@
<string name="conversation_more_recipients">%1$s, %2$s và %3$d người nữa</string>
<string name="conversation_2_recipients">%1$s và %2$s</string>
<string name="conversation_1_recipients">%1$s</string>
<string name="title_favourited_by">Lượt thích tút này</string>
<string name="title_reblogged_by">Lượt đăng lại tút này</string>
<string name="title_favourited_by">Thích bởi</string>
<string name="title_reblogged_by">Đăng lại bởi</string>
<plurals name="reblogs">
<item quantity="other"><b>%1$s</b> Đăng lại</item>
<item quantity="other"><b>%1$s</b> đăng lại</item>
</plurals>
<plurals name="favs">
<item quantity="other"><b>%1$s</b> Thích</item>
<item quantity="other"><b>%1$s</b> thích</item>
</plurals>
<string name="pin_action">Ghim</string>
<string name="unpin_action">Gỡ ghim</string>
@ -393,12 +393,12 @@
<string name="pref_title_absolute_time">Sử dụng thời gian thiết bị</string>
<string name="profile_metadata_content_label">Nội dung</string>
<string name="profile_metadata_label_label">Nhãn</string>
<string name="profile_metadata_add">thêm nội dung</string>
<string name="profile_metadata_add">THÊM NỘI DUNG</string>
<string name="profile_metadata_label">Metadata</string>
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<string name="license_cc_by_4">CC-BY 4.0</string>
<string name="license_apache_2">Giấy phép Apache (xem bên dưới)</string>
<string name="license_description">Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:</string>
<string name="license_description">Tusky có sử dụng những dự án mã nguồn mở sau:</string>
<string name="unreblog_private">Hủy đăng lại</string>
<string name="reblog_private">Đăng lại công khai</string>
<string name="account_moved_description">%1$s đã chuyển sang:</string>
@ -412,11 +412,11 @@
<string name="later">Để sau</string>
<string name="restart_emoji">Bạn cần khởi động lại Tusky để áp dụng các thiết lập</string>
<string name="restart_required">Yêu cầu khởi động lại ứng dụng</string>
<string name="action_open_post">Đọc tút</string>
<string name="action_open_post">Xem tút</string>
<string name="expand_collapse_all_posts">Mở rộng/Thu gọn toàn bộ tút</string>
<string name="performing_lookup_title">Đang tra cứu…</string>
<string name="download_fonts">Bạn cần tải về bộ emoji này trước</string>
<string name="system_default">Mặc định của thiết bị</string>
<string name="system_default">Giống thiết bị</string>
<string name="emoji_style">Emoji</string>
<string name="action_compose_shortcut">Soạn</string>
<string name="compose_save_draft">Lưu nháp\?</string>
@ -443,7 +443,7 @@
<string name="dialog_mute_hide_notifications">Ẩn thông báo</string>
<string name="action_unmute_desc">Bỏ ẩn %1$s</string>
<string name="action_unmute_domain">Bỏ ẩn %1$s</string>
<string name="pref_title_hide_top_toolbar">Ẩn tiêu đề tab</string>
<string name="pref_title_hide_top_toolbar">Ẩn tên tab</string>
<string name="account_note_saved">Đã lưu!</string>
<string name="account_note_hint">Ghi chú về người này</string>
<string name="no_announcements">Chưa có thông báo.</string>
@ -552,7 +552,7 @@
<string name="title_followed_hashtags">Những hashtag theo dõi</string>
<string name="post_edited">Sửa %1$s</string>
<string name="description_post_edited">Đã sửa</string>
<string name="pref_default_post_language">Ngôn ngữ đăng (đồng bộ máy chủ)</string>
<string name="pref_default_post_language">Ngôn ngữ đăng (đ.bộ máy chủ)</string>
<string name="language_display_name_format">%1$s (%2$s)</string>
<string name="error_muting_hashtag_format">Lỗi khi ẩn #%1$s</string>
<string name="error_unmuting_hashtag_format">Lỗi khi bỏ ẩn #%1$s</string>
@ -568,9 +568,9 @@
<string name="status_created_info">Đăng %1$s</string>
<string name="title_edits">Những lượt sửa tút</string>
<string name="a11y_label_loading_thread">Đang tải thảo luận</string>
<string name="action_share_account_link">Chia sẻ URL người dùng</string>
<string name="action_share_account_link">Liên kết trang hồ sơ</string>
<string name="send_account_link_to">Chia sẻ URL người dùng…</string>
<string name="action_share_account_username">Chia sẻ tên người này</string>
<string name="action_share_account_username">Địa chỉ Mastodon</string>
<string name="send_account_username_to">Chia sẻ tên người này…</string>
<string name="account_username_copied">Đã sao chép tên người này</string>
<string name="pref_title_reading_order">Thứ tự đọc</string>
@ -612,12 +612,12 @@
<string name="ui_success_rejected_follow_request">Đã từ chối yêu cầu theo dõi</string>
<string name="status_filtered_show_anyway">Vẫn hiện</string>
<string name="status_filter_placeholder_label_format">Đã lọc: %1$s</string>
<string name="pref_title_account_filter_keywords">Người</string>
<string name="pref_title_account_filter_keywords">Trang hồ sơ</string>
<string name="hint_filter_title">Bộ lọc của tôi</string>
<string name="label_filter_title">Tên bộ lọc</string>
<string name="filter_action_warn">Cảnh báo</string>
<string name="filter_action_hide">Đã ẩn</string>
<string name="filter_description_warn">Ẩn với cảnh báo</string>
<string name="filter_action_warn">Cảnh báo</string>
<string name="filter_action_hide">Lọc ở</string>
<string name="filter_description_warn">Ẩn kèm theo cảnh báo</string>
<string name="filter_description_hide">Ẩn hoàn toàn</string>
<string name="label_filter_action">Hành động</string>
<string name="label_filter_context">Nơi áp dụng</string>
@ -627,7 +627,7 @@
<string name="filter_keyword_addition_title">Thêm từ</string>
<string name="filter_edit_keyword_title">Sửa từ</string>
<string name="filter_description_format">%1$s: %2$s</string>
<string name="pref_title_show_stat_inline">Hiện số tương tác trên tút</string>
<string name="pref_title_show_stat_inline">Hiện số tương tác tút</string>
<string name="help_empty_home">Đây là <b>bảng tin của bạn</b>. Nó sẽ hiện tút gần đây từ những người bạn theo dõi.
\n
\nĐể khám phá mọi người, bạn có thể xem qua ở các bảng tin khác. Ví dụ: [iconics gmd_group] Bảng tin máy chủ của bạn. Hoặc bạn cũng có thể [iconics gmd_search] tìm theo tên người dùng; ví dụ như Tusky.</string>
@ -649,7 +649,7 @@
\nSDK %4$d</string>
<string name="about_account_info_title">Tài khoản của bạn</string>
<string name="about_account_info">\@%1$s@%2$s
\nPhiên bản: %3$s</string>
\nPhiên bản máy chủ: %3$s</string>
<string name="about_copy">Sao chép phiên bản và thông tin thiết bị</string>
<string name="about_copied">Đã sao chép phiên bản và thông tin thiết bị</string>
<string name="about_device_info_title">Thiết bị của bạn</string>
@ -663,11 +663,11 @@
\nTin nhắn riêng được tạo bằng cách chọn tùy chọn kiểu tút [iconics gmd_public] thành [iconics gmd_mail] <i>Nhắn riêng</i> và có nhắc đến một người nào đó.
\n
\nVí dụ: bạn có thể xem hồ sơ của một người và nhấn vào nút [iconics gmd_edit] và đổi kiểu tút. </string>
<string name="help_empty_lists">Đây là <b>danh sách</b>. Bạn có thể tạo nhiều danh sách riêng và thêm người dùng vào đó.
\n
\nBạn chỉ có thể thêm những người BẠN THEO DÕI vào danh sách.
\n
\nDanh sách có thể được sử dụng như một tab trong thiết lập Cá nhân [iconics gmd_account_circle] [iconics gmd_navigate_next] Xếp tab. </string>
<string name="help_empty_lists">Đây là <b>danh sách</b>. Bạn có thể tạo nhiều danh sách riêng và thêm người dùng vào đó.
\n
\nBạn chỉ có thể thêm những người BẠN ĐANG THEO DÕI vào danh sách.
\n
\nDanh sách có thể được sử dụng như một tab trong thiết lập Cá nhân [iconics gmd_account_circle] [iconics gmd_navigate_next] Xếp tab. \u0020</string>
<string name="muting_hashtag_success_format">Đang ẩn hashtag #%1$s như một cảnh báo</string>
<string name="unmuting_hashtag_success_format">Đang bỏ ẩn hashtag #%1$s</string>
<string name="action_view_filter">Xem bộ lọc</string>
@ -676,15 +676,15 @@
<string name="error_blocking_domain">Không thể ẩn %1$s: %2$s</string>
<string name="error_unblocking_domain">Không thể bỏ ẩn %1$s: %2$s</string>
<string name="label_image">Hình ảnh</string>
<string name="app_theme_system_black">Mặc định của thiết bị (Đen)</string>
<string name="app_theme_system_black">Giống thiết bị (Đen)</string>
<string name="title_public_trending_statuses">Tút xu hướng</string>
<string name="list_reply_policy_none">Không ai</string>
<string name="list_reply_policy_list">Người trong danh sách</string>
<string name="list_reply_policy_followed">Người đã theo dõi</string>
<string name="list_reply_policy_label">Hiện lượt trả lời</string>
<string name="pref_title_show_self_boosts">Hiện lượt tự đăng lại</string>
<string name="pref_title_show_self_boosts">Hiện những lượt tự đăng lại</string>
<string name="pref_title_show_self_boosts_description">Ai đó đăng lại tút của chính họ</string>
<string name="pref_title_per_timeline_preferences">Thiết lập từng bảng tin</string>
<string name="pref_title_per_timeline_preferences">Lọc bảng tin</string>
<string name="pref_title_show_notifications_filter">Hiện bộ lọc thông báo</string>
<string name="reply_sending">Đang gửi…</string>
<string name="reply_sending_long">Câu trả lời của bạn đã được gửi đi.</string>
@ -699,12 +699,12 @@
<string name="pref_title_confirm_follows">Hỏi trước khi theo dõi</string>
<string name="url_copied">Đã sao chép liên kết</string>
<string name="confirmation_hashtag_copied">Đã sao chép \'#%1$s\'</string>
<string name="pref_default_reply_privacy">Kiểu trả lời (không đồng bộ máy chủ)</string>
<string name="pref_default_reply_privacy">Kiểu trả lời (không đ.bộ máy chủ)</string>
<string name="error_deleting_filter">Xóa bộ lọc \'%1$s\' không được</string>
<string name="error_saving_filter">Chưa thể lưu bộ lọc \'%1$s\'</string>
<string name="action_follow_hashtag">Theo dõi một hashtag mới</string>
<string name="pref_default_reply_privacy_explanation">Chọn dựa vào tút bạn đang trả lời.</string>
<string name="pref_match_default_post_privacy">Khớp mặc định tút</string>
<string name="pref_default_reply_privacy_explanation">Tự động giống tút bạn đang trả lời</string>
<string name="pref_match_default_post_privacy">Giống mặc định tút</string>
<string name="post_privacy_direct">Nhắn riêng</string>
<string name="label_expires_after">Hết hạn sau</string>
</resources>

View File

@ -19,7 +19,6 @@ package com.keylesspalace.tusky
import androidx.test.ext.junit.runners.AndroidJUnit4
import at.connyduck.calladapter.networkresult.NetworkResult
import com.keylesspalace.tusky.components.filters.EditFilterActivity
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Filter
@ -277,22 +276,6 @@ class FilterV1Test {
)
}
@Test
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
val expiredBySeconds = 3600
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
assert(updatedDuration != null && updatedDuration <= -expiredBySeconds)
}
@Test
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
val expiresInSeconds = 3600
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
assert(updatedDuration != null && updatedDuration > (expiresInSeconds - 60))
}
companion object {
fun mockStatus(
content: String = "",

View File

@ -1,17 +1,17 @@
[versions]
agp = "8.7.0"
androidx-activity = "1.9.2"
agp = "8.7.2"
androidx-activity = "1.9.3"
androidx-appcompat = "1.7.0"
androidx-browser = "1.8.0"
androidx-cardview = "1.0.0"
androidx-constraintlayout = "2.1.4"
androidx-constraintlayout = "2.2.0"
androidx-core = "1.13.1"
androidx-drawerlayout = "1.2.0"
androidx-exifinterface = "1.3.7"
androidx-fragment = "1.8.4"
androidx-fragment = "1.8.5"
androidx-hilt = "1.2.0"
androidx-junit = "1.2.1"
androidx-lifecycle = "2.8.6"
androidx-lifecycle = "2.8.7"
androidx-media3 = "1.4.1"
androidx-paging = "3.3.2"
androidx-preference = "1.2.1"
@ -50,13 +50,13 @@ robolectric = "4.13"
sparkbutton = "4.2.0"
touchimageview = "3.6"
truth = "1.4.4"
turbine = "1.1.0"
turbine = "1.2.0"
unified-push = "2.4.0"
xmlwriter = "1.0.4"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
google-ksp = "com.google.devtools.ksp:2.0.21-1.0.25"
google-ksp = "com.google.devtools.ksp:2.0.21-1.0.26"
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }

File diff suppressed because it is too large Load Diff