diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 2ffcc894b..d0cbc949d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 84fd53502..49894626e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1186,12 +1186,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP); RequestBuilder 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); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt index bf9345807..9d727ee20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -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 . */ + 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) - } - } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt index 9bde72187..881fd70e9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt @@ -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 . */ + 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, expiresInSeconds: Int?): Boolean { + private suspend fun createFilterV1(context: List, 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, expiresInSeconds: Int?): Boolean { + private suspend fun updateFilterV1(context: List, 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] + ) + } + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExpiration.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExpiration.kt new file mode 100644 index 000000000..27d1290d4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExpiration.kt @@ -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 . */ + +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) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt index a2364f6df..2d0c737f9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 2685fda14..e315d5252 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -76,8 +76,6 @@ class CachedTimelineViewModel @Inject constructor( filterModel ) { - private val account = accountManager.activeAccount!! - private var currentPagingSource: PagingSource? = null /** Map from status id to translation. */ diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 41b5eb0bf..ff95bc214 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -304,7 +304,7 @@ class NetworkTimelineViewModel @Inject constructor( } override fun clearWarning(status: StatusViewData.Concrete) { - updateStatusByActionableId(status.id) { + updateStatusByActionableId(status.actionableId) { it.copy(filtered = emptyList()) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 015725660..ff915ea7b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -46,6 +46,8 @@ abstract class TimelineViewModel( private val filterModel: FilterModel ) : ViewModel() { + protected val account = accountManager.activeAccount!! + abstract val statuses: Flow> 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) || diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt index 8f659c57b..13d1afa8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -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 = _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.filter(): List { return filter { status -> - if (status.isDetailed) { + if (status.isDetailed || status.status.account.id == activeAccount.accountId) { true } else { status.filterAction = filterModel.shouldFilterStatus(status.status) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt index c901ebff4..b5f07884e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 1e7801cac..cacaa4e2e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -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, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresInSeconds: Int? + @Field("expires_in") expiresIn: FilterExpiration? ): NetworkResult @FormUrlEncoded @@ -554,7 +555,7 @@ interface MastodonApi { @Field("context[]") context: List, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresInSeconds: Int? + @Field("expires_in") expiresIn: FilterExpiration? ): NetworkResult @DELETE("api/v1/filters/{id}") @@ -566,7 +567,7 @@ interface MastodonApi { @Field("title") title: String, @Field("context[]") context: List, @Field("filter_action") filterAction: String, - @Field("expires_in") expiresInSeconds: Int? + @Field("expires_in") expiresIn: FilterExpiration? ): NetworkResult @FormUrlEncoded @@ -576,7 +577,7 @@ interface MastodonApi { @Field("title") title: String? = null, @Field("context[]") context: List? = null, @Field("filter_action") filterAction: String? = null, - @Field("expires_in") expiresInSeconds: Int? = null + @Field("expires_in") expires: FilterExpiration? = null ): NetworkResult @DELETE("api/v2/filters/{id}") diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 363c10de4..e331c8946 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -52,11 +52,11 @@ Xong ĐĂNG Quay lại - Tiếp tục + TIẾP TỤC Cập nhật Xóa ĐĂNG - Đăng nhập Mastodon + ĐĂNG NHẬP MASTODON Xóa và viết lại tút này\? Xóa tút này\? Bỏ theo dõi người này\? @@ -90,7 +90,7 @@ Đăng lại URL tút với… Đăng lại tút với… Đang lưu vào thiết bị - Tải xuống + Lưu media Đăng lại với tư cách … Mở với tư cách %1$s Chép URL @@ -98,7 +98,7 @@ Mở tập tin #%1$d Links Lượt nhắc tới - Hashtag + HASHTAG Xem lượt thích Xem lượt đăng lại Xem lượt đăng lại @@ -139,7 +139,7 @@ Thích Trang hồ sơ Đóng - Thử lại + THỬ LẠI Xóa & viết lại Xóa Sửa @@ -148,8 +148,8 @@ Ẩn lượt đăng lại Bỏ chặn Chặn - Bỏ theo dõi - Theo dõi + BỎ THEO DÕI + THEO DÕI 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. Soạn tút Thêm @@ -183,36 +183,36 @@ Những tút đã lưu Người theo dõi Theo dõi - Ghim - Lượt trả lời - Tút - Nội dung tút + GHIM + LƯỢT TRẢ LỜI + TÚT + Tút Xếp tab Nhắn riêng Liên hợp Máy chủ Thông báo - Trang chính + Trang chủ Những tút nháp Những tút đã thích Máy chủ là gì\? Hiện xem trước hình ảnh - Hiện những trả lời - Hiện lượt đăng lại + Hiện những tút dạng trả lời + Hiện những lượt đăng lại Trang chủ Lọc bảng tin - Phủ màu media nhạy cảm + Làm mờ media nhạy cảm Ảnh đại diện GIF Icon cho tài khoản Bot Ngôn ngữ Mở luôn trong app Trình duyệt - Mặc định của thiết bị + Giống thiết bị Tự động khi trời tối Đen Sáng Tối - Bộ lọc + Những từ khóa đã lọc Bảng tin Chủ đề Giao diện @@ -254,9 +254,9 @@ Trên màn hình Vị trí menu Không thể lưu thiết lập - Đăng mặc định - Tài khoản nhạy cảm (đồng bộ máy chủ) - Kiểu tút mặc định (đồng bộ máy chủ) + Mặc định khi đăng + Media nhạy cảm (đ.bộ máy chủ) + Kiểu tút (đ.bộ máy chủ) Máy chủ proxy Cổng Bật proxy @@ -268,7 +268,7 @@ Báo lỗi và đề xuất tính năng \nhttps://github.com/tuskyapp/Tusky/issues Website dự án: https://tusky.app - 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 + 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 Powered by Tusky Tusky %1$s Tài khoản bị khóa @@ -288,13 +288,13 @@ Hiện nội dung nhạy cảm Đang theo dõi bạn %1$ds - %1$d phút - %1$d giờ + %1$dp + %1$dh %1$d ngày %1$d năm %1$ds %1$d phút - %1$d giờ + %1$dh%1$ giờ %1$d ngày %1$d năm Yêu cầu theo dõi @@ -322,7 +322,7 @@ Bình chọn Vuốt qua lại giữa các tab Không thể tìm thấy - Người + NGƯỜI Người này thuộc máy chủ khác. Gửi luôn cho máy chủ đó\? 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: Chưa tải được tút @@ -358,8 +358,8 @@ Soạn Soạn tút Áp dụng - Lọc - Xóa hết + LỌC + XÓA HẾT Danh sách Chọn danh sách Hashtag @@ -379,13 +379,13 @@ %1$s, %2$s và %3$d người nữa %1$s và %2$s %1$s - Lượt thích tút này - Lượt đăng lại tút này + Thích bởi + Đăng lại bởi - %1$s Đăng lại + %1$s đăng lại - %1$s Thích + %1$s thích Ghim Gỡ ghim @@ -393,12 +393,12 @@ Sử dụng thời gian thiết bị Nội dung Nhãn - thêm nội dung + THÊM NỘI DUNG Metadata CC-BY-SA 4.0 CC-BY 4.0 Giấy phép Apache (xem bên dưới) - Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: + Tusky có sử dụng những dự án mã nguồn mở sau: Hủy đăng lại Đăng lại công khai %1$s đã chuyển sang: @@ -412,11 +412,11 @@ Để sau Bạn cần khởi động lại Tusky để áp dụng các thiết lập Yêu cầu khởi động lại ứng dụng - Đọc tút + Xem tút Mở rộng/Thu gọn toàn bộ tút Đang tra cứu… Bạn cần tải về bộ emoji này trước - Mặc định của thiết bị + Giống thiết bị Emoji Soạn Lưu nháp\? @@ -443,7 +443,7 @@ Ẩn thông báo Bỏ ẩn %1$s Bỏ ẩn %1$s - Ẩn tiêu đề tab + Ẩn tên tab Đã lưu! Ghi chú về người này Chưa có thông báo. @@ -552,7 +552,7 @@ Những hashtag theo dõi Sửa %1$s Đã sửa - Ngôn ngữ đăng (đồng bộ máy chủ) + Ngôn ngữ đăng (đ.bộ máy chủ) %1$s (%2$s) Lỗi khi ẩn #%1$s Lỗi khi bỏ ẩn #%1$s @@ -568,9 +568,9 @@ Đăng %1$s Những lượt sửa tút Đang tải thảo luận - Chia sẻ URL người dùng + Liên kết trang hồ sơ Chia sẻ URL người dùng… - Chia sẻ tên người này + Địa chỉ Mastodon Chia sẻ tên người này… Đã sao chép tên người này Thứ tự đọc @@ -612,12 +612,12 @@ Đã từ chối yêu cầu theo dõi Vẫn hiện Đã lọc: %1$s - Người + Trang hồ sơ Bộ lọc của tôi Tên bộ lọc - Cảnh báo - Đã ẩn - Ẩn với cảnh báo + Cảnh báo ở + Lọc ở + Ẩn kèm theo cảnh báo Ẩn hoàn toàn Hành động Nơi áp dụng @@ -627,7 +627,7 @@ Thêm từ Sửa từ %1$s: %2$s - Hiện số tương tác trên tút + Hiện số tương tác tút Đây là bảng tin của bạn. 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. @@ -649,7 +649,7 @@ \nSDK %4$d Tài khoản của bạn \@%1$s@%2$s -\nPhiên bản: %3$s +\nPhiên bản máy chủ: %3$s Sao chép phiên bản và thông tin thiết bị Đã sao chép phiên bản và thông tin thiết bị Thiết bị của bạn @@ -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] Nhắn riêng 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. - Đây là danh sách. 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 MÀ 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. + Đây là danh sách. 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 Đang ẩn hashtag #%1$s như một cảnh báo Đang bỏ ẩn hashtag #%1$s Xem bộ lọc @@ -676,15 +676,15 @@ Không thể ẩn %1$s: %2$s Không thể bỏ ẩn %1$s: %2$s Hình ảnh - Mặc định của thiết bị (Đen) + Giống thiết bị (Đen) Tút xu hướng Không ai Người trong danh sách Người đã theo dõi Hiện lượt trả lời - Hiện lượt tự đăng lại + Hiện những lượt tự đăng lại Ai đó đăng lại tút của chính họ - Thiết lập từng bảng tin + Lọc bảng tin Hiện bộ lọc thông báo Đang gửi… Câu trả lời của bạn đã được gửi đi. @@ -699,12 +699,12 @@ Hỏi trước khi theo dõi Đã sao chép liên kết Đã sao chép \'#%1$s\' - Kiểu trả lời (không đồng bộ máy chủ) + Kiểu trả lời (không đ.bộ máy chủ) Xóa bộ lọc \'%1$s\' không được Chưa thể lưu bộ lọc \'%1$s\' Theo dõi một hashtag mới - Chọn dựa vào tút bạn đang trả lời. - Khớp mặc định tút + Tự động giống tút bạn đang trả lời + Giống mặc định tút Nhắn riêng Hết hạn sau \ No newline at end of file diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt index b28447aba..fd3912cb7 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt @@ -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 = "", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b93e18dc..b8c44e513 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9a3363d40..dafd380b2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -38,6 +38,14 @@ + + + + + + + + @@ -67,6 +75,14 @@ + + + + + + + + @@ -359,6 +375,14 @@ + + + + + + + + @@ -367,6 +391,14 @@ + + + + + + + + @@ -506,6 +538,22 @@ + + + + + + + + + + + + + + + + @@ -570,6 +618,22 @@ + + + + + + + + + + + + + + + + @@ -634,6 +698,22 @@ + + + + + + + + + + + + + + + + @@ -764,6 +844,14 @@ + + + + + + + + @@ -818,6 +906,14 @@ + + + + + + + + @@ -925,6 +1021,14 @@ + + + + + + + + @@ -973,6 +1077,14 @@ + + + + + + + + @@ -1027,6 +1139,14 @@ + + + + + + + + @@ -1083,6 +1203,14 @@ + + + + + + + + @@ -1139,6 +1267,14 @@ + + + + + + + + @@ -1192,6 +1328,14 @@ + + + + + + + + @@ -1240,6 +1384,14 @@ + + + + + + + + @@ -1293,6 +1445,14 @@ + + + + + + + + @@ -1358,6 +1518,14 @@ + + + + + + + + @@ -1406,6 +1574,14 @@ + + + + + + + + @@ -1480,6 +1656,14 @@ + + + + + + + + @@ -1528,6 +1712,14 @@ + + + + + + + + @@ -1576,6 +1768,14 @@ + + + + + + + + @@ -1647,6 +1847,17 @@ + + + + + + + + + + + @@ -1695,6 +1906,14 @@ + + + + + + + + @@ -1775,6 +1994,14 @@ + + + + + + + + @@ -1829,6 +2056,14 @@ + + + + + + + + @@ -2244,6 +2479,14 @@ + + + + + + + + @@ -2724,6 +2967,14 @@ + + + + + + + + @@ -2735,6 +2986,14 @@ + + + + + + + + @@ -2855,6 +3114,22 @@ + + + + + + + + + + + + + + + + @@ -2919,6 +3194,22 @@ + + + + + + + + + + + + + + + + @@ -2959,6 +3250,16 @@ + + + + + + + + + + @@ -3023,6 +3324,22 @@ + + + + + + + + + + + + + + + + @@ -3087,6 +3404,22 @@ + + + + + + + + + + + + + + + + @@ -3151,6 +3484,22 @@ + + + + + + + + + + + + + + + + @@ -3215,6 +3564,22 @@ + + + + + + + + + + + + + + + + @@ -3279,6 +3644,22 @@ + + + + + + + + + + + + + + + + @@ -3343,6 +3724,22 @@ + + + + + + + + + + + + + + + + @@ -3407,6 +3804,22 @@ + + + + + + + + + + + + + + + + @@ -3471,6 +3884,22 @@ + + + + + + + + + + + + + + + + @@ -3535,6 +3964,22 @@ + + + + + + + + + + + + + + + + @@ -3599,6 +4044,22 @@ + + + + + + + + + + + + + + + + @@ -3663,6 +4124,22 @@ + + + + + + + + + + + + + + + + @@ -3727,6 +4204,22 @@ + + + + + + + + + + + + + + + + @@ -3839,6 +4332,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3903,6 +4424,22 @@ + + + + + + + + + + + + + + + + @@ -3967,6 +4504,22 @@ + + + + + + + + + + + + + + + + @@ -4031,6 +4584,22 @@ + + + + + + + + + + + + + + + + @@ -4095,6 +4664,22 @@ + + + + + + + + + + + + + + + + @@ -4159,6 +4744,22 @@ + + + + + + + + + + + + + + + + @@ -4223,6 +4824,22 @@ + + + + + + + + + + + + + + + + @@ -4287,6 +4904,22 @@ + + + + + + + + + + + + + + + + @@ -4375,6 +5008,22 @@ + + + + + + + + + + + + + + + + @@ -4439,6 +5088,22 @@ + + + + + + + + + + + + + + + + @@ -4503,6 +5168,22 @@ + + + + + + + + + + + + + + + + @@ -4567,6 +5248,22 @@ + + + + + + + + + + + + + + + + @@ -4655,6 +5352,22 @@ + + + + + + + + + + + + + + + + @@ -4719,6 +5432,22 @@ + + + + + + + + + + + + + + + + @@ -4783,6 +5512,22 @@ + + + + + + + + + + + + + + + + @@ -4847,6 +5592,22 @@ + + + + + + + + + + + + + + + + @@ -4911,6 +5672,22 @@ + + + + + + + + + + + + + + + + @@ -4975,6 +5752,22 @@ + + + + + + + + + + + + + + + + @@ -5039,6 +5832,22 @@ + + + + + + + + + + + + + + + + @@ -5103,6 +5912,22 @@ + + + + + + + + + + + + + + + + @@ -5167,6 +5992,22 @@ + + + + + + + + + + + + + + + + @@ -5231,6 +6072,22 @@ + + + + + + + + + + + + + + + + @@ -5295,6 +6152,22 @@ + + + + + + + + + + + + + + + + @@ -5359,6 +6232,22 @@ + + + + + + + + + + + + + + + + @@ -5423,6 +6312,22 @@ + + + + + + + + + + + + + + + + @@ -5487,6 +6392,22 @@ + + + + + + + + + + + + + + + + @@ -5551,6 +6472,22 @@ + + + + + + + + + + + + + + + + @@ -5615,6 +6552,22 @@ + + + + + + + + + + + + + + + + @@ -5623,6 +6576,22 @@ + + + + + + + + + + + + + + + + @@ -5631,6 +6600,22 @@ + + + + + + + + + + + + + + + + @@ -5695,6 +6680,22 @@ + + + + + + + + + + + + + + + + @@ -5759,6 +6760,22 @@ + + + + + + + + + + + + + + + + @@ -5823,6 +6840,22 @@ + + + + + + + + + + + + + + + + @@ -5887,6 +6920,22 @@ + + + + + + + + + + + + + + + + @@ -5951,6 +7000,22 @@ + + + + + + + + + + + + + + + + @@ -6015,6 +7080,22 @@ + + + + + + + + + + + + + + + + @@ -6079,6 +7160,22 @@ + + + + + + + + + + + + + + + + @@ -6143,6 +7240,22 @@ + + + + + + + + + + + + + + + + @@ -6207,6 +7320,22 @@ + + + + + + + + + + + + + + + + @@ -6271,6 +7400,22 @@ + + + + + + + + + + + + + + + + @@ -6335,6 +7480,22 @@ + + + + + + + + + + + + + + + + @@ -6399,6 +7560,22 @@ + + + + + + + + + + + + + + + + @@ -6463,6 +7640,22 @@ + + + + + + + + + + + + + + + + @@ -6527,6 +7720,22 @@ + + + + + + + + + + + + + + + + @@ -6591,6 +7800,22 @@ + + + + + + + + + + + + + + + + @@ -6655,6 +7880,22 @@ + + + + + + + + + + + + + + + + @@ -6719,6 +7960,22 @@ + + + + + + + + + + + + + + + + @@ -7221,6 +8478,11 @@ + + + + + @@ -7277,6 +8539,14 @@ + + + + + + + + @@ -7359,6 +8629,14 @@ + + + + + + + + @@ -7415,6 +8693,14 @@ + + + + + + + + @@ -7471,6 +8757,14 @@ + + + + + + + + @@ -7527,6 +8821,14 @@ + + + + + + + +