WIP: prefs via androidx.datastore

This commit is contained in:
charlag 2022-03-19 23:28:00 +01:00
parent b7e0494778
commit b1ea335e3e
No known key found for this signature in database
GPG Key ID: 5B96E7C76F0CA558
35 changed files with 433 additions and 310 deletions

View File

@ -129,6 +129,8 @@ dependencies {
implementation "androidx.room:room-rxjava3:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
implementation "androidx.datastore:datastore-core:1.0.0"
// implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.google.android.material:material:1.5.0"

View File

@ -11,11 +11,17 @@ import android.widget.TextView
import androidx.annotation.StringRes
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide
import javax.inject.Inject
class AboutActivity : BottomSheetActivity(), Injectable {
@Inject
lateinit var prefStore: PrefStore
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -48,28 +54,28 @@ class AboutActivity : BottomSheetActivity(), Injectable {
startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java))
}
}
}
private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
val text = SpannableString(context.getText(textId))
val text = SpannableString(context.getText(textId))
Linkify.addLinks(text, Linkify.WEB_URLS)
Linkify.addLinks(text, Linkify.WEB_URLS)
val builder = SpannableStringBuilder(text)
val urlSpans = text.getSpans(0, text.length, URLSpan::class.java)
for (span in urlSpans) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
val flags = builder.getSpanFlags(span)
val builder = SpannableStringBuilder(text)
val urlSpans = text.getSpans(0, text.length, URLSpan::class.java)
for (span in urlSpans) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
val flags = builder.getSpanFlags(span)
val customSpan = NoUnderlineURLSpan(span.url)
val customSpan = NoUnderlineURLSpan(span.url, prefStore.getBlocking().customTabs)
builder.removeSpan(span)
builder.setSpan(customSpan, start, end, flags)
builder.removeSpan(span)
builder.setSpan(customSpan, start, end, flags)
}
setText(builder)
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
}
setText(builder)
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
}

View File

@ -34,7 +34,8 @@ import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.emojify
@ -56,7 +57,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefs: PrefStore
private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
@ -67,8 +68,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private val searchAdapter = SearchAdapter()
private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) }
private val animateAvatar by lazy { prefs.animateAvatars }
private val animateEmojis by lazy { prefs.animateEmojis }
private val animateAvatar by lazy { prefs.getBlocking().animateAvatars }
private val animateEmojis by lazy { prefs.getBlocking().animateEmojis }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -33,6 +33,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.datastore.core.DataStore;
import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
@ -42,6 +43,7 @@ import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.interfaces.PermissionRequester;
import com.keylesspalace.tusky.settings.PrefData;
import com.keylesspalace.tusky.settings.Prefs;
import com.keylesspalace.tusky.util.ThemeUtils;
@ -57,7 +59,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
public AccountManager accountManager;
@Inject
public Prefs prefs;
public DataStore<PrefData> prefStore;
private static final int REQUESTER_NONE = Integer.MAX_VALUE;
private HashMap<Integer, PermissionRequester> requesters;
@ -70,6 +72,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
// There isn't presently a way to globally change the theme of a whole application at
// runtime, just individual activities. So, each activity has to set its theme before any
// views are created.
PrefData prefs = Prefs.getBlocking(prefStore);
String theme = prefs.getAppTheme();
Log.d("activeTheme", theme);
if (theme.equals("black")) {
@ -190,7 +193,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
if (!showActiveAccount && activeAccount != null) {
accounts.remove(activeAccount);
}
AccountSelectionAdapter adapter = new AccountSelectionAdapter(this, this.prefs);
AccountSelectionAdapter adapter = new AccountSelectionAdapter(this, this.prefStore);
adapter.addAll(accounts);
new AlertDialog.Builder(this)

View File

@ -28,6 +28,7 @@ import autodispose2.autoDispose
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.openLink
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import java.net.URI
@ -160,7 +161,7 @@ abstract class BottomSheetActivity : BaseActivity() {
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open fun openLink(url: String) {
(this as Context).openLink(url)
(this as Context).openLink(url, prefStore.getBlocking().customTabs)
}
private fun showQuerySheet() {

View File

@ -76,8 +76,8 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
@ -137,7 +137,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
lateinit var draftHelper: DraftHelper
@Inject
lateinit var prefs: Prefs
lateinit var prefs: PrefStore
private val binding by viewBinding(ActivityMainBinding::inflate)
@ -227,7 +227,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivity(composeIntent)
}
val hideTopToolbar = prefs.hideTopToolbar
val hideTopToolbar = prefStore.getBlocking().hideTopToolbar
binding.mainToolbar.visible(!hideTopToolbar)
loadDrawerAvatar(activeAccount.profilePictureUrl, true)
@ -396,7 +396,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
R.attr.colorBackgroundAccent
)
)
val animateAvatars = prefs.animateAvatars
val animateAvatars = prefStore.getBlocking().animateAvatars
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
@ -580,8 +580,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun setupTabs(selectNotificationTab: Boolean) {
val activeTabLayout = if (prefs.mainNavPosition == "bottom") {
val activeTabLayout = if (prefStore.getBlocking().mainNavPosition == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin =
@ -626,7 +625,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
binding.viewPager.isUserInputEnabled = prefs.enableSwipeForTabs
binding.viewPager.isUserInputEnabled = prefStore.getBlocking().enableSwipeForTabs
onTabSelectedListener?.let {
activeTabLayout.removeOnTabSelectedListener(it)
@ -770,7 +769,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
val animateAvatars = prefs.animateAvatars
val animateAvatars = prefStore.getBlocking().animateAvatars
if (animateAvatars) {
glide.asDrawable()
@ -874,7 +873,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun updateProfiles() {
val animateEmojis = prefs.animateEmojis
val animateEmojis = prefStore.getBlocking().animateEmojis
val profiles: MutableList<IProfile> =
accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get()

View File

@ -20,19 +20,20 @@ import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.settings.makePrefStore
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.ThemeUtils
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import kotlinx.coroutines.runBlocking
import org.conscrypt.Conscrypt
import java.security.Security
import javax.inject.Inject
@ -46,7 +47,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
lateinit var notificationWorkerFactory: NotificationWorkerFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
override fun onCreate() {
// Uncomment me to get StrictMode violation logs
@ -69,14 +70,14 @@ class TuskyApplication : Application(), HasAndroidInjector {
// init the custom emoji fonts
val emojiSelection = prefs.emojiFont
val emojiSelection = prefStore.getBlocking().emojiFont
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
EmojiCompat.init(emojiConfig)
// init night mode
val theme = prefs.appTheme
val theme = prefStore.getBlocking().appTheme
ThemeUtils.setAppNightMode(theme)
RxJavaPlugins.setErrorHandler {
@ -92,9 +93,13 @@ class TuskyApplication : Application(), HasAndroidInjector {
}
override fun attachBaseContext(base: Context) {
// special case: injected field cannot be injected here yet so we create Prefs by hand
localeManager = LocaleManager(Prefs(base))
super.attachBaseContext(localeManager.setLocale(base))
// Special case: injected field cannot be injected here yet so we create Prefs by hand
// Give it a blocking scope so that it will be closed and pref store will be released
runBlocking {
val prefs = makePrefStore(base, this)
localeManager = LocaleManager(prefs)
super.attachBaseContext(localeManager.setLocale(base))
}
}
override fun onConfigurationChanged(newConfig: Configuration) {

View File

@ -20,18 +20,17 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
class AccountSelectionAdapter(
context: Context,
private val prefs: Prefs,
private val prefStore: PrefStore,
) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
@ -44,6 +43,7 @@ class AccountSelectionAdapter(
val account = getItem(position)
if (account != null) {
// TODO: is this even okay to do prefs things for each invocation here?
val prefs = prefStore.getBlocking()
val animateEmojis = prefs.animateEmojis
binding.username.text = account.fullName

View File

@ -91,7 +91,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
timestampInfo.append("");
if (app.getWebsite() != null) {
CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite());
CharSequence text = LinkHelper.createClickableText(
app.getName(),
app.getWebsite(),
false // This seems like a sensible default for clicking on app name
);
timestampInfo.append(text);
timestampInfo.setMovementMethod(LinkMovementMethod.getInstance());
} else {

View File

@ -40,7 +40,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide
@ -68,8 +67,8 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.DefaultTextWatcher
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
@ -97,7 +96,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
@Inject
lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
private val viewModel: AccountViewModel by viewModels { viewModelFactory }
@ -148,6 +147,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
// Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
val prefs = prefStore.getBlocking()
animateAvatar = prefs.animateAvatars
animateEmojis = prefs.animateEmojis
hideFab = prefs.hideFab
@ -189,7 +189,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
accountFieldAdapter = AccountFieldAdapter(
this,
animateEmojis, prefStore.getBlocking().customTabs,
)
binding.accountFieldList.isNestedScrollingEnabled = false
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
binding.accountFieldList.adapter = accountFieldAdapter
@ -215,7 +218,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
}
// If wellbeing mode is enabled, follow stats and posts count should be hidden
val wellbeingEnabled = prefs.hideStatsProfile
val wellbeingEnabled = prefStore.getBlocking().hideStatsProfile
if (wellbeingEnabled) {
binding.accountStatuses.hide()
@ -577,7 +580,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
showingReblogs = relation.showingReblogs
// If wellbeing mode is enabled, "follows you" text should not be visible
val wellbeingEnabled = prefs.hideStatsProfile
val wellbeingEnabled = prefStore.getBlocking().hideStatsProfile
binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)

View File

@ -33,7 +33,8 @@ import com.keylesspalace.tusky.util.setClickableText
class AccountFieldAdapter(
private val linkListener: LinkListener,
private val animateEmojis: Boolean
private val animateEmojis: Boolean,
private val useCustomTabs: Boolean,
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
var emojis: List<Emoji> = emptyList()
@ -55,7 +56,11 @@ class AccountFieldAdapter(
val identityProof = proofOrField.asLeft()
nameTextView.text = identityProof.provider
valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
valueTextView.text = createClickableText(
identityProof.username,
identityProof.profileUrl,
useCustomTabs,
)
valueTextView.movementMethod = LinkMovementMethod.getInstance()

View File

@ -37,6 +37,8 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink
@ -63,6 +65,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
@Inject
lateinit var api: MastodonApi
@Inject
lateinit var prefStore: PrefStore
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var accountId: String
@ -252,7 +257,10 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
}
}
Attachment.Type.UNKNOWN -> {
context?.openLink(items[currentIndex].attachment.url)
context?.openLink(
items[currentIndex].attachment.url,
prefStore.getBlocking().customTabs,
)
}
}
}

View File

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.View
import android.widget.PopupWindow
import androidx.activity.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.BottomSheetActivity
@ -32,8 +31,8 @@ import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
@ -48,7 +47,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
@Inject
lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefs: PrefStore
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
@ -88,8 +87,8 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
binding.announcementsList.addItemDecoration(divider)
val wellbeingEnabled = prefs.hideStatsPosts
val animateEmojis = prefs.animateEmojis
val wellbeingEnabled = prefStore.getBlocking().hideStatsPosts
val animateEmojis = prefStore.getBlocking().animateEmojis
adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis)

View File

@ -50,7 +50,6 @@ import androidx.core.view.ContentInfoCompat
import androidx.core.view.OnReceiveContentListener
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -73,8 +72,9 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefData
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.ComposeTokenizer
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
@ -113,7 +113,7 @@ class ComposeActivity :
@Inject
lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefs: PrefStore
private lateinit var composeOptionsBehavior: BottomSheetBehavior<*>
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
@ -165,6 +165,7 @@ class ComposeActivity :
accountManager.setActiveAccount(accountId)
}
val prefs = prefStore.getBlocking()
val theme = prefs.appTheme
if (theme == "black") {
setTheme(R.style.TuskyDialogActivityBlackTheme)
@ -175,7 +176,7 @@ class ComposeActivity :
// do not do anything when not logged in, activity will be finished in super.onCreate() anyway
val activeAccount = accountManager.activeAccount ?: return
setupAvatar(activeAccount)
setupAvatar(prefs, activeAccount)
val mediaAdapter = MediaPreviewAdapter(
this,
onAddCaption = { item ->
@ -211,7 +212,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
setupComposeField(viewModel.startingText)
setupComposeField(prefs, viewModel.startingText)
setupContentWarningField(composeOptions?.contentWarning)
setupPollView()
applyShareIntent(intent, savedInstanceState)
@ -296,7 +297,7 @@ class ComposeActivity :
binding.composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
}
private fun setupComposeField(startingText: String?) {
private fun setupComposeField(prefs: PrefData, startingText: String?) {
binding.composeEditField.setOnReceiveContentListener(this)
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
@ -314,9 +315,9 @@ class ComposeActivity :
binding.composeEditField.setSelection(binding.composeEditField.length())
val mentionColour = binding.composeEditField.linkTextColors.defaultColor
highlightSpans(binding.composeEditField.text, mentionColour)
highlightSpans(binding.composeEditField.text, mentionColour, prefs.customTabs)
binding.composeEditField.afterTextChanged { editable ->
highlightSpans(editable, mentionColour)
highlightSpans(editable, mentionColour, prefs.customTabs)
updateVisibleCharactersLeft()
}
@ -430,7 +431,7 @@ class ComposeActivity :
}
}
private fun setupAvatar(activeAccount: AccountEntity) {
private fun setupAvatar(prefs: PrefData, activeAccount: AccountEntity) {
val actionBarSizeAttr = intArrayOf(R.attr.actionBarSize)
val a = obtainStyledAttributes(null, actionBarSizeAttr)
val avatarSize = a.getDimensionPixelSize(0, 1)

View File

@ -25,7 +25,6 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadState
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
@ -38,8 +37,8 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
@ -58,7 +57,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
@Inject
lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var prefs: Prefs
lateinit var prefsStore: PrefStore
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
@ -76,6 +75,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val prefs = prefsStore.getBlocking()
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = prefs.animateAvatars,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,

View File

@ -23,7 +23,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
@ -42,8 +41,8 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.viewBinding
@ -62,7 +61,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
lateinit var accountManager: AccountManager
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
@ -109,6 +108,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
}
private fun initStatusesView() {
val prefs = prefStore.getBlocking()
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = false,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,

View File

@ -17,23 +17,23 @@ package com.keylesspalace.tusky.components.search.fragments
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.get
import com.keylesspalace.tusky.settings.getBlocking
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class SearchAccountsFragment : SearchFragment<TimelineAccount>() {
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> {
return SearchAccountsAdapter(
this,
animateAvatars = prefs.animateAvatars,
animateEmojis = prefs.animateEmojis,
animateAvatars = prefStore.getBlocking().animateAvatars,
animateEmojis = prefStore.getBlocking().animateEmojis,
)
}

View File

@ -34,7 +34,6 @@ import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
@ -52,8 +51,8 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.openLink
@ -66,7 +65,7 @@ import javax.inject.Inject
class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener {
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
override val data: Flow<PagingData<StatusViewData.Concrete>>
get() = viewModel.statusesFlow
@ -75,6 +74,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagingDataAdapter<StatusViewData.Concrete, *> {
val prefs = prefStore.getBlocking()
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = prefs.animateAvatars,
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
@ -88,8 +88,14 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
animateEmojis = prefs.animateEmojis,
)
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
binding.searchRecyclerView.addItemDecoration(
DividerItemDecoration(
binding.searchRecyclerView.context,
DividerItemDecoration.VERTICAL
)
)
binding.searchRecyclerView.layoutManager =
LinearLayoutManager(binding.searchRecyclerView.context)
return SearchStatusesAdapter(statusDisplayOptions, this)
}
@ -145,7 +151,10 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
}
}
Attachment.Type.UNKNOWN -> {
context?.openLink(actionable.attachments[attachmentIndex].url)
context?.openLink(
actionable.attachments[attachmentIndex].url,
prefStore.getBlocking().customTabs,
)
}
}
}
@ -241,7 +250,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank()
when (status.visibility) {
Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
val textId = getString(if (status.isPinned()) R.string.unpin_action else R.string.pin_action)
val textId =
getString(if (status.isPinned()) R.string.unpin_action else R.string.pin_action)
menu.add(0, R.id.pin, 1, textId)
}
Status.Visibility.PRIVATE -> {
@ -267,7 +277,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
openAsItem.title = openAsText
}
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
val mutable =
statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
isVisible = mutable
}
@ -290,11 +301,16 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
sendIntent.action = Intent.ACTION_SEND
val stringToShare = statusToShare.account.username +
" - " +
statusToShare.content.toString()
" - " +
statusToShare.content.toString()
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_content_to)))
startActivity(
Intent.createChooser(
sendIntent,
resources.getText(R.string.send_status_content_to)
)
)
return@setOnMenuItemClickListener true
}
R.id.status_share_link -> {
@ -302,11 +318,17 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_link_to)))
startActivity(
Intent.createChooser(
sendIntent,
resources.getText(R.string.send_status_link_to)
)
)
return@setOnMenuItemClickListener true
}
R.id.status_copy_link -> {
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipboard =
requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl))
return@setOnMenuItemClickListener true
}
@ -402,7 +424,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
val uri = Uri.parse(url)
val filename = uri.lastPathSegment
val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadManager =
requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
downloadManager.enqueue(request)
@ -415,13 +438,24 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadAllMedia(status)
} else {
Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show()
Toast.makeText(
context,
R.string.error_media_download_permission,
Toast.LENGTH_SHORT
).show()
}
}
}
private fun openReportPage(accountId: String, accountUsername: String, statusId: String) {
startActivity(ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId))
startActivity(
ReportActivity.getIntent(
requireContext(),
accountId,
accountUsername,
statusId
)
)
}
private fun showConfirmDeleteDialog(id: String, position: Int) {
@ -471,7 +505,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
},
{ error ->
Log.w("SearchStatusesFragment", "error deleting status", error)
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT)
.show()
}
)
}

View File

@ -26,7 +26,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -56,7 +55,9 @@ import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.get
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -67,10 +68,13 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlow
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.concurrent.timer
class TimelineFragment :
SFragment(),
@ -90,7 +94,7 @@ class TimelineFragment :
lateinit var accountManager: AccountManager
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
private val viewModel: TimelineViewModel by lazy {
if (kind == TimelineViewModel.Kind.HOME) {
@ -140,6 +144,7 @@ class TimelineFragment :
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
val prefs = prefStore.getBlocking()
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = prefs.animateAvatars,
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
@ -238,7 +243,7 @@ class TimelineFragment :
}
if (actionButtonPresent()) {
hideFab = prefs.hideFab
hideFab = prefStore.getBlocking().hideFab
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
val composeButton = (activity as ActionButtonActivity).actionButton
@ -259,20 +264,21 @@ class TimelineFragment :
}
}
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe { event ->
when (event) {
is PreferenceChangedEvent -> {
onPreferenceChanged(event.preferenceKey)
}
is StatusComposedEvent -> {
val status = event.status
handleStatusComposeEvent(status)
lifecycleScope.launch {
eventHub.events
.asFlow()
.collect { event ->
when (event) {
is PreferenceChangedEvent -> {
onPreferenceChanged(event.preferenceKey)
}
is StatusComposedEvent -> {
val status = event.status
handleStatusComposeEvent(status)
}
}
}
}
}
}
private fun setupSwipeRefreshLayout() {
@ -283,7 +289,7 @@ class TimelineFragment :
private fun setupRecyclerView() {
binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos ->
ListStatusAccessibilityDelegate(binding.recyclerView, this, prefStore) { pos ->
if (pos in 0 until adapter.itemCount) {
adapter.peek(pos)
} else {
@ -413,10 +419,10 @@ class TimelineFragment :
super.viewAccount(id)
}
private fun onPreferenceChanged(key: String) {
private suspend fun onPreferenceChanged(key: String) {
when (key) {
PrefKeys.FAB_HIDE -> {
hideFab = prefs.hideFab
hideFab = prefStore.get().hideFab
}
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
@ -471,7 +477,9 @@ class TimelineFragment :
if (talkBackWasEnabled && !wasEnabled) {
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
startUpdateTimestamp()
lifecycleScope.launchWhenResumed {
startUpdateTimestamp()
}
}
/**
@ -479,8 +487,8 @@ class TimelineFragment :
* If setting absoluteTimeView is false
* Auto dispose observable on pause
*/
private fun startUpdateTimestamp() {
val useAbsoluteTime = prefs.useAbsoluteTime
private suspend fun startUpdateTimestamp() {
val useAbsoluteTime = prefStore.get().useAbsoluteTime
if (!useAbsoluteTime) {
Observable.interval(1, TimeUnit.MINUTES)
.observeOn(AndroidSchedulers.mainThread())

View File

@ -40,7 +40,7 @@ import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.inc
import com.keylesspalace.tusky.viewdata.StatusViewData
@ -61,7 +61,7 @@ class CachedTimelineViewModel @Inject constructor(
private val api: MastodonApi,
eventHub: EventHub,
accountManager: AccountManager,
prefs: Prefs,
prefs: PrefStore,
filterModel: FilterModel,
private val db: AppDatabase,
private val gson: Gson

View File

@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.getDomain
import com.keylesspalace.tusky.util.inc
@ -58,7 +58,7 @@ class NetworkTimelineViewModel @Inject constructor(
private val api: MastodonApi,
eventHub: EventHub,
accountManager: AccountManager,
prefs: Prefs,
prefs: PrefStore,
filterModel: FilterModel
) : TimelineViewModel(timelineCases, api, eventHub, accountManager, prefs, filterModel) {

View File

@ -40,7 +40,9 @@ import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.get
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@ -53,7 +55,7 @@ abstract class TimelineViewModel(
private val api: MastodonApi,
private val eventHub: EventHub,
protected val accountManager: AccountManager,
private val prefs: Prefs,
private val prefStore: PrefStore,
private val filterModel: FilterModel
) : ViewModel() {
@ -81,6 +83,7 @@ abstract class TimelineViewModel(
this.tags = tags
if (kind == Kind.HOME) {
val prefs = prefStore.getBlocking()
filterRemoveReplies = !prefs.tabFilterHomeReplies
filterRemoveReblogs = !prefs.tabFilterHomeBoosts
}
@ -177,10 +180,10 @@ abstract class TimelineViewModel(
filterModel.shouldFilterStatus(status.actionableStatus)
}
private fun onPreferenceChanged(key: String) {
private suspend fun onPreferenceChanged(key: String) {
when (key) {
PrefKeys.TAB_FILTER_HOME_REPLIES -> {
val filter = prefs.tabFilterHomeReplies
val filter = prefStore.get().tabFilterHomeReplies
val oldRemoveReplies = filterRemoveReplies
filterRemoveReplies = kind == Kind.HOME && !filter
if (oldRemoveReplies != filterRemoveReplies) {
@ -188,7 +191,7 @@ abstract class TimelineViewModel(
}
}
PrefKeys.TAB_FILTER_HOME_BOOSTS -> {
val filter = prefs.tabFilterHomeBoosts
val filter = prefStore.get().tabFilterHomeBoosts
val oldRemoveReblogs = filterRemoveReblogs
filterRemoveReblogs = kind == Kind.HOME && !filter
if (oldRemoveReblogs != filterRemoveReblogs) {
@ -230,7 +233,7 @@ abstract class TimelineViewModel(
}
}
private fun handleEvent(event: Event) {
private suspend fun handleEvent(event: Event) {
when (event) {
is FavoriteEvent -> handleFavEvent(event)
is ReblogEvent -> handleReblogEvent(event)

View File

@ -18,13 +18,19 @@ package com.keylesspalace.tusky.di
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.settings.PrefData
import com.keylesspalace.tusky.settings.makePrefStore
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
/**
@ -45,6 +51,13 @@ class AppModule {
return PreferenceManager.getDefaultSharedPreferences(app)
}
@Provides
@Singleton
fun providesDataStore(app: Application): DataStore<PrefData> {
// Scope is the copy of default one
return makePrefStore(app, CoroutineScope(Dispatchers.IO + SupervisorJob()))
}
@Provides
@Singleton
fun providesDatabase(appContext: Context, converters: Converters): AppDatabase {
@ -52,16 +65,34 @@ class AppModule {
.addTypeConverter(converters)
.allowMainThreadQueries()
.addMigrations(
AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
AppDatabase.MIGRATION_2_3,
AppDatabase.MIGRATION_3_4,
AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6,
AppDatabase.MIGRATION_6_7,
AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9,
AppDatabase.MIGRATION_9_10,
AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12,
AppDatabase.MIGRATION_12_13,
AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14,
AppDatabase.MIGRATION_14_15,
AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17,
AppDatabase.MIGRATION_17_18,
AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20,
AppDatabase.MIGRATION_20_21,
AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23,
AppDatabase.MIGRATION_23_24,
AppDatabase.MIGRATION_24_25,
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
AppDatabase.MIGRATION_26_27,
AppDatabase.MIGRATION_27_28,
AppDatabase.MIGRATION_28_29,
AppDatabase.MIGRATION_29_30
)
.build()

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.di
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.text.Spanned
import com.google.gson.Gson
@ -26,8 +25,8 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import dagger.Module
import dagger.Provides
import okhttp3.Cache
@ -63,8 +62,9 @@ class NetworkModule {
fun providesHttpClient(
accountManager: AccountManager,
context: Context,
prefs: Prefs,
prefStore: PrefStore,
): OkHttpClient {
val prefs = prefStore.getBlocking()
val httpProxyEnabled = prefs.httpProxyEnabled
val httpServer = prefs.httpProxyServer
val httpPort = prefs.httpProxyPort.toIntOrNull() ?: -1

View File

@ -20,7 +20,6 @@ import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -46,8 +45,8 @@ import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
@ -67,7 +66,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var prefs: Prefs
lateinit var prefStore: PrefStore
private val binding by viewBinding(FragmentAccountListBinding::bind)
@ -95,6 +94,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
val prefs = prefStore.getBlocking()
val animateAvatar = prefs.animateAvatars
val animateEmojis = prefs.animateEmojis

View File

@ -15,6 +15,10 @@
package com.keylesspalace.tusky.fragment;
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
import static autodispose2.AutoDispose.autoDisposable;
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
@ -36,8 +40,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.arch.core.util.Function;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.util.Pair;
import androidx.datastore.core.DataStore;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
@ -71,6 +75,7 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.settings.PrefData;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.settings.Prefs;
import com.keylesspalace.tusky.util.CardViewMode;
@ -111,10 +116,6 @@ import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
import static autodispose2.AutoDispose.autoDisposable;
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
@ -157,7 +158,7 @@ public class NotificationsFragment extends SFragment implements
@Inject
EventHub eventHub;
@Inject
Prefs prefs;
DataStore<PrefData> prefStore;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
@ -216,6 +217,7 @@ public class NotificationsFragment extends SFragment implements
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
PrefData prefs = Prefs.getBlocking(prefStore);
boolean showNotificationsFilterSetting = prefs.getShowNotificationsFilter();
//Clear notifications on filter visibility change to force refresh
if (showNotificationsFilterSetting != showNotificationsFilter)
@ -239,7 +241,7 @@ public class NotificationsFragment extends SFragment implements
layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAccessibilityDelegateCompat(
new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> {
new ListStatusAccessibilityDelegate(recyclerView, this, prefStore, (pos) -> {
NotificationViewData notification = notifications.getPairedItemOrNull(pos);
// We support replies only for now
if (notification instanceof NotificationViewData.Concrete) {
@ -329,7 +331,8 @@ public class NotificationsFragment extends SFragment implements
// guaranteed to be set until then.
// Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides
// the compose button on down-scroll.
hideFab = prefs.getHideFab();
hideFab = Prefs.getBlocking(prefStore).getHideFab();
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
@ -799,7 +802,7 @@ public class NotificationsFragment extends SFragment implements
private void onPreferenceChanged(String key) {
switch (key) {
case PrefKeys.FAB_HIDE: {
hideFab = prefs.getHideFab();
hideFab = Prefs.getBlocking(prefStore).getHideFab();
break;
}
case PrefKeys.MEDIA_PREVIEW_ENABLED: {
@ -812,7 +815,7 @@ public class NotificationsFragment extends SFragment implements
}
case PrefKeys.SHOW_NOTIFICATIONS_FILTER: {
if (isAdded()) {
showNotificationsFilter = prefs.getShowNotificationsFilter();
showNotificationsFilter = Prefs.getBlocking(prefStore).getShowNotificationsFilter();
updateFilterVisibility();
fullyRefreshWithProgressBar(true);
}
@ -1224,7 +1227,7 @@ public class NotificationsFragment extends SFragment implements
* Auto dispose observable on pause
*/
private void startUpdateTimestamp() {
boolean useAbsoluteTime = prefs.getUseAbsoluteTime();
boolean useAbsoluteTime = Prefs.getBlocking(prefStore).getUseAbsoluteTime();
if (!useAbsoluteTime) {
Observable.interval(1, TimeUnit.MINUTES)
.observeOn(AndroidSchedulers.mainThread())

View File

@ -36,6 +36,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.ViewCompat;
import androidx.datastore.core.DataStore;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
@ -55,6 +56,8 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.settings.PrefData;
import com.keylesspalace.tusky.settings.Prefs;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.view.MuteAccountDialog;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
@ -91,6 +94,8 @@ public abstract class SFragment extends Fragment implements Injectable {
public AccountManager accountManager;
@Inject
public TimelineCases timelineCases;
@Inject
public DataStore<PrefData> prefStore;
private static final String TAG = "SFragment";
@ -363,7 +368,11 @@ public abstract class SFragment extends Fragment implements Injectable {
}
default:
case UNKNOWN: {
LinkHelper.openLink(requireContext(), active.getAttachment().getUrl());
LinkHelper.openLink(
requireContext(),
active.getAttachment().getUrl(),
Prefs.getBlocking(prefStore).getCustomTabs()
);
break;
}
}

View File

@ -27,6 +27,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.datastore.core.DataStore;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DividerItemDecoration;
@ -57,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.FilterModel;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.settings.PrefData;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.settings.Prefs;
import com.keylesspalace.tusky.util.CardViewMode;
@ -94,7 +96,7 @@ public final class ViewThreadFragment extends SFragment implements
@Inject
public FilterModel filterModel;
@Inject
public Prefs prefs;
public DataStore<PrefData> prefStore;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
@ -132,6 +134,7 @@ public final class ViewThreadFragment extends SFragment implements
thisThreadsStatusId = getArguments().getString("id");
PrefData prefs = Prefs.getBlocking(prefStore);
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
prefs.getAnimateAvatars(),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
@ -164,7 +167,7 @@ public final class ViewThreadFragment extends SFragment implements
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAccessibilityDelegateCompat(
new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull));
new ListStatusAccessibilityDelegate(recyclerView, this, prefStore, statuses::getPairedItemOrNull));
DividerItemDecoration divider = new DividerItemDecoration(
context, layoutManager.getOrientation());
recyclerView.addItemDecoration(divider);
@ -330,7 +333,7 @@ public final class ViewThreadFragment extends SFragment implements
// already viewing the status with this url
// probably just a preview federated and the user is clicking again to view more -> open the browser
// this can happen with some friendica statuses
LinkHelper.openLink(requireContext(), url);
LinkHelper.openLink(requireContext(), url, Prefs.getBlocking(prefStore).getCustomTabs());
return;
}
super.onViewUrl(url);

View File

@ -1,129 +1,81 @@
@file:JvmName("Prefs")
package com.keylesspalace.tusky.settings
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.Keep
import androidx.preference.PreferenceManager
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.Serializer
import com.google.gson.Gson
import com.keylesspalace.tusky.util.ThemeUtils
import javax.inject.Inject
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.InputStream
import java.io.OutputStream
// TODO: do we every plan to use them as writable properties?
// TODO: is this enough to preserve fields/names?
// TODO: what about observing changes? Do we keep PrefKeys and reference them?
@Keep
class Prefs @Inject constructor(context: Context) {
// TODO: not sure if should be lazy or non-cached at all
private val sharedPreferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
data class PrefData(
var appTheme: String = ThemeUtils.APP_THEME_DEFAULT,
var emojiFont: Int = 0,
val hideFab: Boolean = false,
var language: String = "default",
val statusTextSize: String = "medium",
val mainNavPosition: String? = null,
val hideTopToolbar: Boolean = false,
val animateAvatars: Boolean = true,
val useAbsoluteTime: Boolean = false,
val showBotOverlay: Boolean = true,
val useBlurhash: Boolean = true,
val showNotificationsFilter: Boolean = true,
val showCardsInTimelines: Boolean = false,
val confirmReblogs: Boolean = true,
val confirmFavourites: Boolean = false,
val enableSwipeForTabs: Boolean = true,
val customTabs: Boolean = false,
val hideStatsPosts: Boolean = false,
val hideStatsProfile: Boolean = false,
val animateEmojis: Boolean = false,
val tabFilterHomeReplies: Boolean = true,
val tabFilterHomeBoosts: Boolean = true,
var appTheme by stringProperty(ThemeUtils.APP_THEME_DEFAULT)
var emojiFont by intProperty(0, "selected_emoji_font")
val hideFab by booleanProperty(false, "fabHide")
var language by stringProperty(defaultValue = "default")
val statusTextSize by stringProperty("medium")
val mainNavPosition by stringProperty()
val hideTopToolbar by booleanProperty(false)
val httpProxyEnabled: Boolean = false,
val httpProxyServer: String = "",
val httpProxyPort: String = ""
)
val animateAvatars by booleanProperty(false, "animateGifAvatars")
val useAbsoluteTime by booleanProperty(false, "absoluteTimeView")
val showBotOverlay by booleanProperty(true)
val useBlurhash by booleanProperty(true)
val showNotificationsFilter by booleanProperty(true)
val showCardsInTimelines by booleanProperty(false)
val confirmReblogs by booleanProperty(true)
val confirmFavourites by booleanProperty(false)
val enableSwipeForTabs by booleanProperty(true)
val customTabs by booleanProperty(false)
val hideStatsPosts by booleanProperty(false, "wellbeingHideStatsPosts")
val hideStatsProfile by booleanProperty(false, "wellbeingHideStatsProfile")
val animateEmojis by booleanProperty(false, "animateCustomEmojis")
val tabFilterHomeReplies by booleanProperty(true)
val tabFilterHomeBoosts by booleanProperty(true)
abstract class GsonSerializer<T>(
private val classOfData: Class<T>,
) : Serializer<T> {
private val gson = Gson()
val httpProxyEnabled by booleanProperty(false)
val httpProxyServer by stringProperty(defaultValue = "")
val httpProxyPort by stringProperty(defaultValue = "")
private fun stringProperty(overrideName: String? = null) =
StringProperty(sharedPreferences, overrideName)
private fun stringProperty(
defaultValue: String,
overrideName: String? = null,
): ReadWriteProperty<Prefs, String> =
this.stringProperty(overrideName).withDefault(defaultValue)
private fun booleanProperty(
defaultValue: Boolean,
overrideName: String? = null,
) = BooleanProperty(sharedPreferences, overrideName, defaultValue)
private fun intProperty(
defaultValue: Int,
overrideName: String? = null,
) = IntProperty(sharedPreferences, overrideName, defaultValue)
}
private fun <T, P> ReadWriteProperty<T, P?>.withDefault(
default: P
): ReadWriteProperty<T, P> = object : ReadWriteProperty<T, P> {
override fun getValue(thisRef: T, property: KProperty<*>): P {
return this@withDefault.getValue(thisRef, property) ?: default
override suspend fun readFrom(input: InputStream): T {
return gson.fromJson(input.reader(), classOfData)
}
override fun setValue(thisRef: T, property: KProperty<*>, value: P) {
this@withDefault.setValue(thisRef, property, value)
override suspend fun writeTo(t: T, output: OutputStream) {
gson.toJson(t, output.writer())
}
}
private class StringProperty(
private val sharedPreferences: SharedPreferences,
private val overrideName: String?,
) : ReadWriteProperty<Prefs, String?> {
override fun getValue(thisRef: Prefs, property: KProperty<*>): String? {
return sharedPreferences.getString(overrideName ?: property.name, null)
}
override fun setValue(thisRef: Prefs, property: KProperty<*>, value: String?) {
sharedPreferences.edit().putString(overrideName ?: property.name, value)
.apply()
}
object PrefDataSerializer : GsonSerializer<PrefData>(PrefData::class.java) {
override val defaultValue: PrefData
get() = PrefData()
}
private class BooleanProperty(
private val sharedPreferences: SharedPreferences,
private val overrideName: String?,
private val defaultValue: Boolean,
) : ReadWriteProperty<Prefs, Boolean> {
override fun getValue(thisRef: Prefs, property: KProperty<*>): Boolean {
return sharedPreferences.getBoolean(
overrideName ?: property.name,
defaultValue,
)
}
fun <T> DataStore<T>.getBlocking() = runBlocking { this@getBlocking.data.first() }
suspend fun <T> DataStore<T>.get() = this.data.first()
override fun setValue(thisRef: Prefs, property: KProperty<*>, value: Boolean) {
sharedPreferences.edit().putBoolean(overrideName ?: property.name, value)
.apply()
}
}
typealias PrefStore = DataStore<PrefData>
private class IntProperty(
private val sharedPreferences: SharedPreferences,
private val overrideName: String?,
private val defaultValue: Int,
) : ReadWriteProperty<Prefs, Int> {
override fun getValue(thisRef: Prefs, property: KProperty<*>): Int {
return sharedPreferences.getInt(
overrideName ?: property.name,
defaultValue,
)
/** Exposed for special cases, please inject singleton instead! */
fun makePrefStore(context: Context, scope: CoroutineScope): PrefStore {
return DataStoreFactory.create(
PrefDataSerializer,
scope = scope,
) {
// Would love to use dataStoreFile() here but it needs app context which we might not have
// yet.
File(context.filesDir, "datastore/prefs.json")
}
override fun setValue(thisRef: Prefs, property: KProperty<*>, value: Int) {
sharedPreferences.edit().putInt(overrideName ?: property.name, value)
.apply()
}
}
}

View File

@ -32,12 +32,13 @@ import androidx.annotation.VisibleForTesting
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.settings.makePrefStore
fun getDomain(urlString: String?): String {
val host = urlString?.toUri()?.host
@ -57,7 +58,13 @@ fun getDomain(urlString: String?): String {
* @param mentions any '@' mentions which are known to be in the content
* @param listener to notify about particular spans that are clicked
*/
fun setClickableText(view: TextView, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener) {
fun setClickableText(
view: TextView,
content: CharSequence,
mentions: List<Mention>,
tags: List<HashTag>?,
listener: LinkListener
) {
view.text = SpannableStringBuilder.valueOf(content).apply {
getSpans(0, content.length, URLSpan::class.java).forEach {
setClickableText(it, this, mentions, tags, listener)
@ -83,7 +90,8 @@ fun setClickableText(
'#' -> getCustomSpanForTag(text, tags, span, listener)
'@' -> getCustomSpanForMention(mentions, span, listener)
else -> null
} ?: object : NoUnderlineURLSpan(span.url) {
} ?: object : NoUnderlineURLSpan(span.url, false) {
// It doesn't matter what we pass for customTabs because we override onCLick() anyway
override fun onClick(view: View) = listener.onViewUrl(url)
}
@ -107,23 +115,36 @@ fun getTagName(text: CharSequence, tags: List<HashTag>?): String? {
}
}
private fun getCustomSpanForTag(text: CharSequence, tags: List<HashTag>?, span: URLSpan, listener: LinkListener): ClickableSpan? {
private fun getCustomSpanForTag(
text: CharSequence,
tags: List<HashTag>?,
span: URLSpan,
listener: LinkListener
): ClickableSpan? {
return getTagName(text, tags)?.let {
object : NoUnderlineURLSpan(span.url) {
object : NoUnderlineURLSpan(span.url, false) {
override fun onClick(view: View) = listener.onViewTag(it)
}
}
}
private fun getCustomSpanForMention(mentions: List<Mention>, span: URLSpan, listener: LinkListener): ClickableSpan? {
private fun getCustomSpanForMention(
mentions: List<Mention>,
span: URLSpan,
listener: LinkListener
): ClickableSpan? {
// https://github.com/tuskyapp/Tusky/pull/2339
return mentions.firstOrNull { it.url == span.url }?.let {
getCustomSpanForMentionUrl(span.url, it.id, listener)
}
}
private fun getCustomSpanForMentionUrl(url: String, mentionId: String, listener: LinkListener): ClickableSpan {
return object : NoUnderlineURLSpan(url) {
private fun getCustomSpanForMentionUrl(
url: String,
mentionId: String,
listener: LinkListener
): ClickableSpan {
return object : NoUnderlineURLSpan(url, false) {
override fun onClick(view: View) = listener.onViewAccount(mentionId)
}
}
@ -171,9 +192,14 @@ fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: Lin
view.movementMethod = LinkMovementMethod.getInstance()
}
fun createClickableText(text: String, link: String): CharSequence {
fun createClickableText(text: String, link: String, useCustomTabs: Boolean): CharSequence {
return SpannableStringBuilder(text).apply {
setSpan(NoUnderlineURLSpan(link), 0, text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
setSpan(
NoUnderlineURLSpan(link, useCustomTabs),
0,
text.length,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
)
}
}
@ -182,12 +208,11 @@ fun createClickableText(text: String, link: String): CharSequence {
*
* @receiver the Context to open the link from
* @param url a string containing the url to open
* @param customTabs whether to use customs tabs or open link in system browser
*/
fun Context.openLink(url: String) {
fun Context.openLink(url: String, customTabs: Boolean) {
val uri = url.toUri().normalizeScheme()
val useCustomTabs = Prefs(this).customTabs
if (useCustomTabs) {
if (customTabs) {
openLinkInCustomTab(uri, this)
} else {
openLinkInBrowser(uri, this)

View File

@ -18,6 +18,8 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.entity.Status.Companion.MAX_MEDIA_ATTACHMENTS
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlin.math.min
@ -29,10 +31,11 @@ fun interface StatusProvider {
class ListStatusAccessibilityDelegate(
private val recyclerView: RecyclerView,
private val statusActionListener: StatusActionListener,
private val statusProvider: StatusProvider
private val prefStore: PrefStore,
private val statusProvider: StatusProvider,
) : RecyclerViewAccessibilityDelegate(recyclerView) {
private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE)
as AccessibilityManager
as AccessibilityManager
override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate
@ -182,7 +185,12 @@ class ListStatusAccessibilityDelegate(
android.R.layout.simple_list_item_1,
textLinks
)
) { _, which -> host.context.openLink(links[which].link) }
) { _, which ->
host.context.openLink(
links[which].link,
prefStore.getBlocking().customTabs,
)
}
.show()
.let { forceFocus(it.listView) }
}

View File

@ -17,15 +17,16 @@ package com.keylesspalace.tusky.util
import android.content.Context
import android.content.res.Configuration
import com.keylesspalace.tusky.settings.Prefs
import com.keylesspalace.tusky.settings.PrefStore
import com.keylesspalace.tusky.settings.getBlocking
import java.util.*
class LocaleManager(
private val prefs: Prefs,
private val prefs: PrefStore,
) {
fun setLocale(context: Context): Context {
val language = prefs.language
val language = prefs.getBlocking().language
if (language == "default") {
return context
}

View File

@ -20,7 +20,8 @@ import android.text.style.URLSpan
import android.view.View
open class NoUnderlineURLSpan(
url: String
url: String,
private val useCustomTabs: Boolean,
) : URLSpan(url) {
override fun updateDrawState(ds: TextPaint) {
@ -29,6 +30,6 @@ open class NoUnderlineURLSpan(
}
override fun onClick(view: View) {
view.context.openLink(url)
view.context.openLink(url, useCustomTabs)
}
}

View File

@ -124,16 +124,23 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P
}
}
private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, start: Int, end: Int): CharacterStyle {
private fun getSpan(
matchType: FoundMatchType,
string: String,
colour: Int,
start: Int,
end: Int,
useCustomTabs: Boolean,
): CharacterStyle {
return when (matchType) {
FoundMatchType.HTTP_URL -> NoUnderlineURLSpan(string.substring(start, end))
FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end))
FoundMatchType.HTTP_URL -> NoUnderlineURLSpan(string.substring(start, end), useCustomTabs)
FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end), useCustomTabs)
else -> ForegroundColorSpan(colour)
}
}
/** Takes text containing mentions and hashtags and urls and makes them the given colour. */
fun highlightSpans(text: Spannable, colour: Int) {
fun highlightSpans(text: Spannable, colour: Int, useCustomTabs: Boolean) {
// Strip all existing colour spans.
for (spanClass in spanClasses) {
clearSpans(text, spanClass)
@ -150,7 +157,7 @@ fun highlightSpans(text: Spannable, colour: Int) {
start = found.start
end = found.end
if (start >= 0 && end > start) {
text.setSpan(getSpan(found.matchType, string, colour, start, end), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
text.setSpan(getSpan(found.matchType, string, colour, start, end, useCustomTabs), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
start += finders[found.matchType]!!.searchPrefixWidth
}
}

View File

@ -50,7 +50,7 @@ class LicenseCard
binding.licenseCardLink.hide()
} else {
binding.licenseCardLink.text = link
setOnClickListener { context.openLink(link) }
setOnClickListener { context.openLink(link, false) }
}
}
}