Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
171f69a35d
|
@ -108,7 +108,7 @@ ext.retrofitVersion = '2.9.0'
|
|||
ext.okhttpVersion = '4.8.1'
|
||||
ext.glideVersion = '4.11.0'
|
||||
ext.daggerVersion = '2.28.3'
|
||||
ext.materialdrawerVersion = '8.1.4'
|
||||
ext.materialdrawerVersion = '8.1.8'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
|
@ -143,7 +143,7 @@ dependencies {
|
|||
implementation "androidx.room:room-rxjava2:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
implementation "com.google.android.material:material:1.2.0"
|
||||
implementation "com.google.android.material:material:1.2.1"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
|
@ -191,7 +191,5 @@ dependencies {
|
|||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
||||
|
||||
debugImplementation "im.dino:dbinspector:4.0.0@aar"
|
||||
|
||||
implementation 'net.accelf:easter:1.0.2'
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".SavedTootActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"/>
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
@ -105,7 +105,7 @@
|
|||
<activity
|
||||
android:name=".components.compose.ComposeActivity"
|
||||
android:theme="@style/TuskyDialogActivityTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity
|
||||
android:name=".ViewThreadActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
@ -145,6 +145,7 @@
|
|||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver
|
||||
|
@ -180,9 +181,9 @@
|
|||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:exported="false"
|
||||
tools:node="remove"/>
|
||||
tools:node="remove" />
|
||||
|
||||
<activity android:name="net.accelf.yuito.AccessTokenLoginActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.graphics.Color
|
|||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
|
@ -34,7 +35,6 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
|
@ -132,6 +132,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
if (viewModel.isSelf) {
|
||||
updateButtons()
|
||||
saveNoteInfo.hide()
|
||||
} else {
|
||||
saveNoteInfo.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,7 +314,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
* Subscribe to data loaded at the view model
|
||||
*/
|
||||
private fun subscribeObservables() {
|
||||
viewModel.accountData.observe(this, Observer {
|
||||
viewModel.accountData.observe(this) {
|
||||
when (it) {
|
||||
is Success -> onAccountChanged(it.data)
|
||||
is Error -> {
|
||||
|
@ -320,8 +323,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
viewModel.relationshipData.observe(this, Observer {
|
||||
}
|
||||
viewModel.relationshipData.observe(this) {
|
||||
val relation = it?.data
|
||||
if (relation != null) {
|
||||
onRelationshipChanged(relation)
|
||||
|
@ -333,12 +336,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
.show()
|
||||
}
|
||||
|
||||
})
|
||||
viewModel.accountFieldData.observe(this, Observer {
|
||||
}
|
||||
viewModel.accountFieldData.observe(this, {
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
})
|
||||
viewModel.noteSaved.observe(this) {
|
||||
saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -349,7 +354,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
viewModel.refresh()
|
||||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(this, Observer { isRefreshing ->
|
||||
viewModel.isRefreshing.observe(this, { isRefreshing ->
|
||||
swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
})
|
||||
swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
|
@ -407,7 +412,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
|
||||
accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar)
|
||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
avatarView.transitionName = account.avatar
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||
|
@ -533,9 +538,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
accountFollowsYouTextView.visible(relation.followedBy)
|
||||
|
||||
accountNoteTextInputLayout.visible(relation.note != null)
|
||||
accountNoteTextInputLayout.editText?.setText(relation.note)
|
||||
|
||||
// add the listener late to avoid it firing on the first change
|
||||
accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher)
|
||||
accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher)
|
||||
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private val noteWatcher = object: DefaultTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
viewModel.noteChanged(s.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFollowButton() {
|
||||
if (viewModel.isSelf) {
|
||||
accountFollowButton.setText(R.string.action_edit_own_profile)
|
||||
|
@ -705,9 +723,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
loadedAccount?.let {
|
||||
showMuteAccountDialog(
|
||||
this,
|
||||
it.username,
|
||||
{ notifications -> viewModel.muteAccount(notifications) }
|
||||
)
|
||||
it.username
|
||||
) { notifications ->
|
||||
viewModel.muteAccount(notifications)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.unmuteAccount()
|
||||
|
|
|
@ -32,7 +32,6 @@ import androidx.core.app.ActivityCompat
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
|
@ -41,8 +40,6 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
|
@ -123,7 +120,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
viewModel.obtainProfile()
|
||||
|
||||
viewModel.profileData.observe(this, Observer<Resource<Account>> { profileRes ->
|
||||
viewModel.profileData.observe(this) { profileRes ->
|
||||
when (profileRes) {
|
||||
is Success -> {
|
||||
val me = profileRes.data
|
||||
|
@ -164,10 +161,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.obtainInstance()
|
||||
viewModel.instanceData.observe(this, Observer<Resource<Instance>> { result ->
|
||||
viewModel.instanceData.observe(this) { result ->
|
||||
when (result) {
|
||||
is Success -> {
|
||||
val instance = result.data
|
||||
|
@ -176,12 +173,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true)
|
||||
observeImage(viewModel.headerData, headerPreview, headerProgressBar, false)
|
||||
|
||||
viewModel.saveData.observe(this, Observer<Resource<Nothing>> {
|
||||
viewModel.saveData.observe(this, {
|
||||
when(it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
|
@ -216,7 +213,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean) {
|
||||
liveData.observe(this, Observer<Resource<Bitmap>> {
|
||||
liveData.observe(this, {
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
|
|
|
@ -41,10 +41,8 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
|
|||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.color
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.mikepenz.iconics.utils.toIconicsColor
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
|
|
|
@ -55,8 +55,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
||||
|
@ -80,6 +80,9 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.mikepenz.materialdrawer.holder.BadgeStyle
|
||||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
|
@ -122,6 +125,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private var notificationTabPosition = 0
|
||||
private var onTabSelectedListener: OnTabSelectedListener? = null
|
||||
|
||||
private var unreadAnnouncementsCount = 0
|
||||
|
||||
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||
|
||||
private val emojiInitCallback = object : InitCallback() {
|
||||
|
@ -215,6 +220,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
* drawer, though, because its callback touches the header in the drawer. */
|
||||
fetchUserInfo()
|
||||
|
||||
fetchAnnouncements()
|
||||
|
||||
setupTabs(showNotificationTab)
|
||||
|
||||
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
|
||||
|
@ -241,6 +248,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
when (event) {
|
||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||
is MainTabsChangedEvent -> setupTabs(false)
|
||||
is AnnouncementReadEvent -> {
|
||||
unreadAnnouncementsCount--
|
||||
updateAnnouncementsBadge()
|
||||
}
|
||||
}
|
||||
viewQuickToot.handleEvent(event)
|
||||
}
|
||||
|
@ -446,6 +457,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
identifier = DRAWER_ITEM_ANNOUNCEMENTS
|
||||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_account_preferences
|
||||
|
@ -487,7 +510,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
)
|
||||
|
||||
if(addSearchButton) {
|
||||
if (addSearchButton) {
|
||||
mainDrawer.addItemsAtPosition(4,
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
|
@ -537,7 +560,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
private fun setupTabs(selectNotificationTab: Boolean): ArrayList<PopupMenu> {
|
||||
|
||||
val activeTabLayout = if(preferences.getString("mainNavPosition", "top") == "bottom") {
|
||||
val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
|
||||
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
|
||||
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
|
||||
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
|
||||
|
@ -554,7 +577,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
val adapter = MainPagerAdapter(tabs, this)
|
||||
viewPager.adapter = adapter
|
||||
TabLayoutMediator(activeTabLayout, viewPager, TabConfigurationStrategy { _: TabLayout.Tab?, _: Int -> }).attach()
|
||||
TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach()
|
||||
activeTabLayout.removeAllTabs()
|
||||
val popups = ArrayList<PopupMenu>()
|
||||
for (i in tabs.indices) {
|
||||
|
@ -805,10 +828,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.into(object : CustomTarget<Drawable>(){
|
||||
.into(object : CustomTarget<Drawable>() {
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
mainToolbar.navigationIcon = resource
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
mainToolbar.navigationIcon = placeholder
|
||||
}
|
||||
|
@ -837,6 +861,25 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
updateShortcut(this, accountManager.activeAccount!!)
|
||||
}
|
||||
|
||||
private fun fetchAnnouncements() {
|
||||
mastodonApi.listAnnouncements(false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ announcements ->
|
||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||
updateAnnouncementsBadge()
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateAnnouncementsBadge() {
|
||||
mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount == 0) null else unreadAnnouncementsCount.toString()))
|
||||
}
|
||||
|
||||
private fun updateProfiles() {
|
||||
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
||||
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header))
|
||||
|
@ -871,6 +914,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private const val TAG = "MainActivity" // logging tag
|
||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||
private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10
|
||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||
const val STATUS_URL = "statusUrl"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType);
|
||||
|
||||
ComposeOptions composeOptions = new ComposeOptions(
|
||||
/*scheduledTootUid*/null,
|
||||
item.getUid(),
|
||||
item.getText(),
|
||||
jsonUrls,
|
||||
|
@ -182,6 +183,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
/*scheduledAt*/null,
|
||||
/*sensitive*/null,
|
||||
/*poll*/null,
|
||||
/* modifiedInitialState */ true,
|
||||
false
|
||||
);
|
||||
Intent intent = ComposeActivity.startIntent(this, composeOptions);
|
||||
|
|
|
@ -15,19 +15,25 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
||||
import com.keylesspalace.tusky.adapter.ListSelectionAdapter
|
||||
import com.keylesspalace.tusky.adapter.TabAdapter
|
||||
|
@ -129,19 +135,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
|
||||
touchHelper.attachToRecyclerView(currentTabsRecyclerView)
|
||||
|
||||
|
||||
actionButton.setOnClickListener {
|
||||
actionButton.isExpanded = true
|
||||
toggleFab(true)
|
||||
}
|
||||
|
||||
scrim.setOnClickListener {
|
||||
actionButton.isExpanded = false
|
||||
toggleFab(false)
|
||||
}
|
||||
|
||||
maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT)
|
||||
|
||||
updateAvailableTabs()
|
||||
|
||||
}
|
||||
|
||||
override fun onTabAdded(tab: TabData) {
|
||||
|
@ -150,7 +154,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
return
|
||||
}
|
||||
|
||||
actionButton.isExpanded = false
|
||||
toggleFab(false)
|
||||
|
||||
if (tab.id == HASHTAG) {
|
||||
showAddHashtagDialog()
|
||||
|
@ -188,6 +192,22 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
currentTabsAdapter.notifyItemChanged(tabPosition)
|
||||
}
|
||||
|
||||
private fun toggleFab(expand: Boolean) {
|
||||
val transition = MaterialContainerTransform().apply {
|
||||
startView = if (expand) actionButton else sheet
|
||||
val endView: View = if (expand) sheet else actionButton
|
||||
this.endView = endView
|
||||
addTarget(endView)
|
||||
scrimColor = Color.TRANSPARENT
|
||||
setPathMotion(MaterialArcMotion())
|
||||
}
|
||||
|
||||
TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition)
|
||||
actionButton.visible(!expand)
|
||||
sheet.visible(expand)
|
||||
scrim.visible(expand)
|
||||
}
|
||||
|
||||
private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) {
|
||||
|
||||
val frameLayout = FrameLayout(this)
|
||||
|
@ -318,10 +338,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (actionButton.isExpanded) {
|
||||
actionButton.isExpanded = false
|
||||
} else {
|
||||
if (actionButton.isVisible) {
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
toggleFab(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ import com.bumptech.glide.request.FutureTarget
|
|||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||
import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
|
@ -68,7 +68,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
companion object {
|
||||
private const val EXTRA_ATTACHMENTS = "attachments"
|
||||
private const val EXTRA_ATTACHMENT_INDEX = "index"
|
||||
private const val EXTRA_AVATAR_URL = "avatar"
|
||||
private const val EXTRA_SINGLE_IMAGE_URL = "single_image"
|
||||
private const val TAG = "ViewMediaActivity"
|
||||
|
||||
@JvmStatic
|
||||
|
@ -79,9 +79,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
return intent
|
||||
}
|
||||
|
||||
fun newAvatarIntent(context: Context, url: String): Intent {
|
||||
@JvmStatic
|
||||
fun newSingleImageIntent(context: Context, url: String): Intent {
|
||||
val intent = Intent(context, ViewMediaActivity::class.java)
|
||||
intent.putExtra(EXTRA_AVATAR_URL, url)
|
||||
intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +92,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
|
||||
private var attachments: ArrayList<AttachmentViewData>? = null
|
||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||
private var imageUrl: String? = null
|
||||
|
||||
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
||||
this.toolbarVisibilityListeners.add(listener)
|
||||
|
@ -117,10 +119,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
ImagePagerAdapter(this, realAttachs, initialPosition)
|
||||
|
||||
} else {
|
||||
val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL)
|
||||
?: throw IllegalArgumentException("attachment list or avatar url has to be set")
|
||||
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
|
||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||
|
||||
AvatarImagePagerAdapter(this, avatarUrl)
|
||||
SingleImagePagerAdapter(this, imageUrl!!)
|
||||
}
|
||||
|
||||
viewPager.adapter = adapter
|
||||
|
@ -161,11 +163,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (attachments != null) {
|
||||
menuInflater.inflate(R.menu.view_media_toolbar, menu)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
menuInflater.inflate(R.menu.view_media_toolbar, menu)
|
||||
// We don't support 'open status' from single image views
|
||||
menu?.findItem(R.id.action_open_status)?.isVisible = (attachments != null)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
|
@ -213,7 +214,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
private fun downloadMedia() {
|
||||
val url = attachments!![viewPager.currentItem].attachment.url
|
||||
val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url
|
||||
val filename = Uri.parse(url).lastPathSegment
|
||||
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
|
||||
|
||||
|
@ -240,8 +241,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
private fun copyLink() {
|
||||
val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url))
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
|
||||
}
|
||||
|
||||
private fun shareMedia() {
|
||||
|
@ -251,13 +253,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
return
|
||||
}
|
||||
|
||||
val attachment = attachments!![viewPager.currentItem].attachment
|
||||
when (attachment.type) {
|
||||
Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
|
||||
Attachment.Type.AUDIO,
|
||||
Attachment.Type.VIDEO,
|
||||
Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
|
||||
else -> Log.e(TAG, "Unknown media format for sharing.")
|
||||
if (imageUrl != null) {
|
||||
shareImage(directory, imageUrl!!)
|
||||
} else {
|
||||
val attachment = attachments!![viewPager.currentItem].attachment
|
||||
when (attachment.type) {
|
||||
Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
|
||||
Attachment.Type.AUDIO,
|
||||
Attachment.Type.VIDEO,
|
||||
Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
|
||||
else -> Log.e(TAG, "Unknown media format for sharing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import androidx.core.text.BidiFormatter
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
|
||||
|
||||
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
|
||||
|
@ -15,13 +21,16 @@ internal class FollowRequestViewHolder(itemView: View, private val showHeader: B
|
|||
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
|
||||
.getBoolean("animateGifAvatars", false)
|
||||
|
||||
fun setupWithAccount(account: Account, formatter: BidiFormatter?) {
|
||||
fun setupWithAccount(account: Account) {
|
||||
id = account.id
|
||||
val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name
|
||||
val wrappedName = account.name.unicodeWrap()
|
||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView)
|
||||
itemView.displayNameTextView.text = emojifiedName
|
||||
if (showHeader) {
|
||||
itemView.notificationTextView?.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName)
|
||||
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
|
||||
itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply {
|
||||
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}.emojify(account.emojis, itemView)
|
||||
}
|
||||
itemView.notificationTextView?.visible(showHeader)
|
||||
val format = itemView.context.getString(R.string.status_username_format)
|
||||
|
|
|
@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
|||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position), null);
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
@ -53,6 +52,7 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
|||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
@ -90,7 +90,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private StatusActionListener statusListener;
|
||||
private NotificationActionListener notificationActionListener;
|
||||
private AccountActionListener accountActionListener;
|
||||
private BidiFormatter bidiFormatter;
|
||||
private AdapterDataSource<NotificationViewData> dataSource;
|
||||
|
||||
public NotificationsAdapter(String accountId,
|
||||
|
@ -106,7 +105,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
this.statusListener = statusListener;
|
||||
this.notificationActionListener = notificationActionListener;
|
||||
this.accountActionListener = accountActionListener;
|
||||
bidiFormatter = BidiFormatter.getInstance();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -208,7 +206,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
concreteNotificaton.getAccount().getAvatar());
|
||||
}
|
||||
|
||||
holder.setMessage(concreteNotificaton, statusListener, bidiFormatter);
|
||||
holder.setMessage(concreteNotificaton, statusListener);
|
||||
holder.setupButtons(notificationActionListener,
|
||||
concreteNotificaton.getAccount().getId(),
|
||||
concreteNotificaton.getId());
|
||||
|
@ -225,7 +223,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_FOLLOW: {
|
||||
if (payloadForHolder == null) {
|
||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||
holder.setMessage(concreteNotificaton.getAccount(), bidiFormatter);
|
||||
holder.setMessage(concreteNotificaton.getAccount());
|
||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
||||
}
|
||||
break;
|
||||
|
@ -233,7 +231,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
||||
if (payloadForHolder == null) {
|
||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
|
||||
holder.setupWithAccount(concreteNotificaton.getAccount());
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
@ -330,11 +328,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
void setMessage(Account account, BidiFormatter bidiFormatter) {
|
||||
void setMessage(Account account) {
|
||||
Context context = message.getContext();
|
||||
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
String wrappedDisplayName = bidiFormatter.unicodeWrap(account.getName());
|
||||
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
||||
String wholeMessage = String.format(format, wrappedDisplayName);
|
||||
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message);
|
||||
message.setText(emojifiedMessage);
|
||||
|
@ -467,10 +465,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
}
|
||||
|
||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
|
||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
|
||||
this.statusViewData = notificationViewData.getStatusViewData();
|
||||
|
||||
String displayName = bidiFormatter.unicodeWrap(notificationViewData.getAccount().getName());
|
||||
String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName());
|
||||
Notification.Type type = notificationViewData.getType();
|
||||
|
||||
Context context = message.getContext();
|
||||
|
|
|
@ -37,18 +37,21 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
|
|||
private var votersCount: Int? = null
|
||||
private var mode = RESULT
|
||||
private var emojis: List<Emoji> = emptyList()
|
||||
private var resultClickListener: View.OnClickListener? = null
|
||||
|
||||
fun setup(
|
||||
options: List<PollOptionViewData>,
|
||||
voteCount: Int,
|
||||
votersCount: Int?,
|
||||
emojis: List<Emoji>,
|
||||
mode: Int) {
|
||||
mode: Int,
|
||||
resultClickListener: View.OnClickListener?) {
|
||||
this.pollOptions = options
|
||||
this.voteCount = voteCount
|
||||
this.votersCount = votersCount
|
||||
this.emojis = emojis
|
||||
this.mode = mode
|
||||
this.resultClickListener = resultClickListener
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
@ -84,7 +87,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
|
|||
val level = percent * 100
|
||||
|
||||
holder.resultTextView.background.level = level
|
||||
|
||||
holder.resultTextView.setOnClickListener(resultClickListener)
|
||||
}
|
||||
SINGLE -> {
|
||||
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton)
|
||||
|
|
|
@ -27,10 +27,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Attachment.Focus;
|
||||
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
||||
|
@ -697,25 +699,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
final String accountId,
|
||||
final String statusContent,
|
||||
final boolean isNotestock,
|
||||
final String acct,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
|
||||
avatar.setOnClickListener(v -> {
|
||||
View.OnClickListener profileButtonClickListener = button -> {
|
||||
if (isNotestock) {
|
||||
listener.onViewUrl(accountId, accountId);
|
||||
} else {
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
});
|
||||
View.OnClickListener viewAccountListener = v -> {
|
||||
if (isNotestock) {
|
||||
listener.onViewUrl(acct, acct);
|
||||
} else {
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
};
|
||||
displayName.setOnClickListener(viewAccountListener);
|
||||
username.setOnClickListener(viewAccountListener);
|
||||
|
||||
avatar.setOnClickListener(profileButtonClickListener);
|
||||
displayName.setOnClickListener(profileButtonClickListener);
|
||||
username.setOnClickListener(profileButtonClickListener);
|
||||
|
||||
replyButton.setOnClickListener(v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
|
@ -856,11 +852,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
if (cardView != null) {
|
||||
setupCard(status, statusDisplayOptions.cardViewMode());
|
||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
|
||||
}
|
||||
|
||||
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
|
||||
status.isNotestock(), status.getNickname(), statusDisplayOptions);
|
||||
status.isNotestock(), statusDisplayOptions);
|
||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
||||
|
||||
|
@ -1038,12 +1034,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
if (expired || poll.getVoted()) {
|
||||
// no voting possible
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT);
|
||||
View.OnClickListener viewThreadListener = v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onViewThread(position);
|
||||
}
|
||||
};
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener);
|
||||
|
||||
pollButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
// voting possible
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null);
|
||||
|
||||
pollButton.setVisibility(View.VISIBLE);
|
||||
|
||||
|
@ -1087,15 +1089,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
|
||||
} else {
|
||||
String pollDuration = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
|
||||
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration);
|
||||
pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
||||
}
|
||||
|
||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode) {
|
||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
|
||||
if (cardViewMode != CardViewMode.NONE &&
|
||||
status.getAttachments().size() == 0 &&
|
||||
status.getCard() != null &&
|
||||
|
@ -1117,7 +1118,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
cardUrl.setText(card.getUrl());
|
||||
|
||||
if (!TextUtils.isEmpty(card.getImage())) {
|
||||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||
// so let's blur the preview in that case
|
||||
// If media previews are disabled, show placeholder for cards as well
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||
|
||||
int topLeftRadius = 0;
|
||||
int topRightRadius = 0;
|
||||
|
@ -1148,12 +1152,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
bottomLeftRadius = radius;
|
||||
}
|
||||
|
||||
RequestBuilder<Drawable> builder = Glide.with(cardImage).load(card.getImage());
|
||||
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
|
||||
}
|
||||
builder.transform(
|
||||
new CenterCrop(),
|
||||
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
|
||||
)
|
||||
.into(cardImage);
|
||||
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||
int radius = cardImage.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.card_radius);
|
||||
|
||||
Glide.with(cardImage)
|
||||
.load(card.getImage())
|
||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
Glide.with(cardImage).load(decodeBlurHash(card.getBlurhash()))
|
||||
.transform(
|
||||
new CenterCrop(),
|
||||
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
|
||||
new GranularRoundedCorners(radius, 0, 0, radius)
|
||||
)
|
||||
.into(cardImage);
|
||||
} else {
|
||||
|
@ -1166,7 +1187,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
||||
}
|
||||
|
||||
cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext()));
|
||||
View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext());
|
||||
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
||||
|
||||
cardInfo.setOnClickListener(visitLink);
|
||||
// View embedded photos in our image viewer instead of opening the browser
|
||||
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbed_url()) ?
|
||||
openImage :
|
||||
visitLink);
|
||||
|
||||
cardView.setClipToOutline(true);
|
||||
} else {
|
||||
cardView.setVisibility(View.GONE);
|
||||
|
|
|
@ -110,7 +110,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status
|
||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
|
||||
if (payloads == null) {
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
|
||||
|
|
|
@ -21,5 +21,6 @@ data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
|||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
|
||||
data class QuickReplyEvent(val status: Status) : Dispatchable
|
||||
data class StreamUpdateEvent(val status: Status, val targetKind: TimelineFragment.Kind, val targetIdentifier: String?, val first: Boolean) : Dispatchable
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.announcements
|
||||
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.size
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Announcement
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import kotlinx.android.synthetic.main.item_announcement.view.*
|
||||
|
||||
interface AnnouncementActionListener {
|
||||
fun openReactionPicker(announcementId: String, target: View)
|
||||
fun addReaction(announcementId: String, name: String)
|
||||
fun removeReaction(announcementId: String, name: String)
|
||||
}
|
||||
|
||||
class AnnouncementAdapter(
|
||||
private var items: List<Announcement> = emptyList(),
|
||||
private val listener: AnnouncementActionListener
|
||||
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_announcement, parent, false)
|
||||
return AnnouncementViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) {
|
||||
viewHolder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
fun updateList(items: List<Announcement>) {
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val text: TextView = view.text
|
||||
private val chips: ChipGroup = view.chipGroup
|
||||
private val addReactionChip: Chip = view.addReactionChip
|
||||
|
||||
fun bind(item: Announcement) {
|
||||
text.text = item.content
|
||||
|
||||
item.reactions.forEachIndexed { i, reaction ->
|
||||
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||
?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
||||
isCheckable = true
|
||||
checkedIcon = null
|
||||
chips.addView(this, i)
|
||||
})
|
||||
.apply {
|
||||
val emojiText = if (reaction.url == null) {
|
||||
reaction.name
|
||||
} else {
|
||||
view.context.getString(R.string.emoji_shortcode_format, reaction.name)
|
||||
}
|
||||
text = ("$emojiText ${reaction.count}")
|
||||
.emojify(
|
||||
listOf(Emoji(
|
||||
reaction.name,
|
||||
reaction.url ?: "",
|
||||
reaction.staticUrl ?: "",
|
||||
null
|
||||
)),
|
||||
this
|
||||
)
|
||||
|
||||
isChecked = reaction.me
|
||||
|
||||
setOnClickListener {
|
||||
if (reaction.me) {
|
||||
listener.removeReaction(item.id, reaction.name)
|
||||
} else {
|
||||
listener.addReaction(item.id, reaction.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (chips.size - 1 > item.reactions.size) {
|
||||
chips.removeViewAt(item.reactions.size)
|
||||
}
|
||||
|
||||
addReactionChip.setOnClickListener {
|
||||
listener.openReactionPicker(item.id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.announcements
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.PopupWindow
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.view.EmojiPicker
|
||||
import kotlinx.android.synthetic.main.activity_announcements.*
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsActivity : BaseActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
|
||||
|
||||
private val adapter = AnnouncementAdapter(emptyList(), this)
|
||||
|
||||
private val picker by lazy { EmojiPicker(this) }
|
||||
private val pickerDialog by lazy {
|
||||
PopupWindow(this)
|
||||
.apply {
|
||||
contentView = picker
|
||||
isFocusable = true
|
||||
setOnDismissListener {
|
||||
currentAnnouncementId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
private var currentAnnouncementId: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_announcements)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
title = getString(R.string.title_announcements)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements)
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
|
||||
announcementsList.setHasFixedSize(true)
|
||||
announcementsList.layoutManager = LinearLayoutManager(this)
|
||||
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
announcementsList.addItemDecoration(divider)
|
||||
announcementsList.adapter = adapter
|
||||
|
||||
viewModel.announcements.observe(this, Observer {
|
||||
when (it) {
|
||||
is Success -> {
|
||||
progressBar.hide()
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
if (it.data.isNullOrEmpty()) {
|
||||
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
|
||||
errorMessageView.show()
|
||||
} else {
|
||||
errorMessageView.hide()
|
||||
}
|
||||
adapter.updateList(it.data ?: listOf())
|
||||
}
|
||||
is Loading -> {
|
||||
errorMessageView.hide()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
refreshAnnouncements()
|
||||
}
|
||||
errorMessageView.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.emojis.observe(this, Observer {
|
||||
picker.adapter = EmojiAdapter(it, this)
|
||||
})
|
||||
|
||||
viewModel.load()
|
||||
progressBar.show()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun refreshAnnouncements() {
|
||||
viewModel.load()
|
||||
swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun openReactionPicker(announcementId: String, target: View) {
|
||||
currentAnnouncementId = announcementId
|
||||
pickerDialog.showAsDropDown(target)
|
||||
}
|
||||
|
||||
override fun onEmojiSelected(shortcode: String) {
|
||||
viewModel.addReaction(currentAnnouncementId!!, shortcode)
|
||||
pickerDialog.dismiss()
|
||||
}
|
||||
|
||||
override fun addReaction(announcementId: String, name: String) {
|
||||
viewModel.addReaction(announcementId, name)
|
||||
}
|
||||
|
||||
override fun removeReaction(announcementId: String, name: String) {
|
||||
viewModel.removeReaction(announcementId, name)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.announcements
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.InstanceEntity
|
||||
import com.keylesspalace.tusky.entity.Announcement
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
|
||||
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
||||
|
||||
private val emojisMutable = MutableLiveData<List<Emoji>>()
|
||||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||
|
||||
init {
|
||||
Singles.zip(
|
||||
mastodonApi.getCustomEmojis(),
|
||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext(
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right<InstanceEntity, Instance>(it) }
|
||||
)
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().maxTootChars,
|
||||
either.asRight().pollLimits?.maxOptions,
|
||||
either.asRight().pollLimits?.maxOptionChars,
|
||||
either.asRight().version
|
||||
)
|
||||
}
|
||||
.doOnSuccess {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.subscribe({
|
||||
emojisMutable.postValue(it.emojiList)
|
||||
}, {
|
||||
Log.w(TAG, "Failed to get custom emojis.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
announcementsMutable.postValue(Loading())
|
||||
mastodonApi.listAnnouncements()
|
||||
.subscribe({
|
||||
announcementsMutable.postValue(Success(it))
|
||||
it.filter { announcement -> !announcement.read }
|
||||
.forEach { announcement ->
|
||||
mastodonApi.dismissAnnouncement(announcement.id)
|
||||
.subscribe(
|
||||
{
|
||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
||||
},
|
||||
{ throwable ->
|
||||
Log.d(TAG, "Failed to mark announcement as read.", throwable)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}, {
|
||||
announcementsMutable.postValue(Error(cause = it))
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun addReaction(announcementId: String, name: String) {
|
||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
||||
announcement.reactions.map { reaction ->
|
||||
if (reaction.name == name) {
|
||||
reaction.copy(
|
||||
count = reaction.count + 1,
|
||||
me = true
|
||||
)
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listOf(
|
||||
*announcement.reactions.toTypedArray(),
|
||||
emojis.value!!.find { emoji -> emoji.shortcode == name }
|
||||
!!.run {
|
||||
Announcement.Reaction(
|
||||
name,
|
||||
1,
|
||||
true,
|
||||
url,
|
||||
staticUrl
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
announcement
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun removeReaction(announcementId: String, name: String) {
|
||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = announcement.reactions.mapNotNull { reaction ->
|
||||
if (reaction.name == name) {
|
||||
if (reaction.count > 1) {
|
||||
reaction.copy(
|
||||
count = reaction.count - 1,
|
||||
me = false
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
announcement
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AnnouncementsViewModel"
|
||||
}
|
||||
}
|
|
@ -52,9 +52,7 @@ import androidx.core.view.inputmethod.InputContentInfoCompat
|
|||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
@ -157,7 +155,7 @@ class ComposeActivity : BaseActivity(),
|
|||
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||
* based on what the intent from the reply request passes. */
|
||||
if (intent != null) {
|
||||
this.composeOptions = intent.getParcelableExtra<ComposeOptions?>(COMPOSE_OPTIONS_EXTRA)
|
||||
this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
||||
viewModel.setup(composeOptions)
|
||||
setupReplyViews(composeOptions?.replyingStatusAuthor)
|
||||
setupQuoteView(composeOptions?.quoteStatusAuthor)
|
||||
|
@ -374,7 +372,7 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
viewModel.media.observe { media ->
|
||||
mediaAdapter.submitList(media)
|
||||
if(media.size != mediaCount) {
|
||||
if (media.size != mediaCount) {
|
||||
mediaCount = media.size
|
||||
composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
||||
|
@ -384,8 +382,8 @@ class ComposeActivity : BaseActivity(),
|
|||
pollPreview.visible(poll != null)
|
||||
poll?.let(pollPreview::setPoll)
|
||||
}
|
||||
viewModel.scheduledAt.observe {scheduledAt ->
|
||||
if(scheduledAt == null) {
|
||||
viewModel.scheduledAt.observe { scheduledAt ->
|
||||
if (scheduledAt == null) {
|
||||
composeScheduleView.resetSchedule()
|
||||
} else {
|
||||
composeScheduleView.setDateTime(scheduledAt)
|
||||
|
@ -436,7 +434,6 @@ class ComposeActivity : BaseActivity(),
|
|||
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
|
||||
emojiBehavior = BottomSheetBehavior.from(emojiView)
|
||||
|
||||
emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false)
|
||||
enableButton(composeEmojiButton, clickable = false, colorActive = false)
|
||||
|
||||
// Setup the interface buttons.
|
||||
|
@ -645,7 +642,7 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
private fun onScheduleClick() {
|
||||
if(viewModel.scheduledAt.value == null) {
|
||||
if (viewModel.scheduledAt.value == null) {
|
||||
composeScheduleView.openPickDateDialog()
|
||||
} else {
|
||||
showScheduleView()
|
||||
|
@ -777,7 +774,15 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
private fun updateVisibleCharactersLeft() {
|
||||
composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", maximumTootCharacters - calculateTextLength())
|
||||
val remainingLength = maximumTootCharacters - calculateTextLength()
|
||||
composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
||||
|
||||
val textColor = if (remainingLength < 0) {
|
||||
ContextCompat.getColor(this, R.color.tusky_red)
|
||||
} else {
|
||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
}
|
||||
composeCharactersLeftView.setTextColor(textColor)
|
||||
}
|
||||
|
||||
private fun onContentWarningChanged() {
|
||||
|
@ -803,9 +808,9 @@ class ComposeActivity : BaseActivity(),
|
|||
// Verify the returned content's type is of the correct MIME type
|
||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
||||
|
||||
if(supported) {
|
||||
if (supported) {
|
||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||
if(lacksPermission) {
|
||||
if (lacksPermission) {
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
|
@ -835,11 +840,14 @@ class ComposeActivity : BaseActivity(),
|
|||
if (checkboxUseDefaultText.isChecked) {
|
||||
contentText += " ${editTextDefaultText.text}"
|
||||
}
|
||||
finishingUploadDialog = ProgressDialog.show(
|
||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||
getString(R.string.dialog_message_uploading_media), true, true)
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(this, Observer {
|
||||
if (viewModel.media.value!!.isNotEmpty()) {
|
||||
finishingUploadDialog = ProgressDialog.show(
|
||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||
getString(R.string.dialog_message_uploading_media), true, true)
|
||||
}
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(this, {
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
})
|
||||
|
@ -860,7 +868,7 @@ class ComposeActivity : BaseActivity(),
|
|||
Snackbar.LENGTH_SHORT).apply {
|
||||
|
||||
}
|
||||
bar.setAction(R.string.action_retry) { onMediaPick()}
|
||||
bar.setAction(R.string.action_retry) { onMediaPick() }
|
||||
//necessary so snackbar is shown over everything
|
||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
bar.show()
|
||||
|
@ -1002,7 +1010,7 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
Log.d(TAG, event.toString())
|
||||
if(event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (event.isCtrlPressed) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
// send toot by pressing CTRL + ENTER
|
||||
|
@ -1092,6 +1100,7 @@ class ComposeActivity : BaseActivity(),
|
|||
@Parcelize
|
||||
data class ComposeOptions(
|
||||
// Let's keep fields var until all consumers are Kotlin
|
||||
var scheduledTootUid: String? = null,
|
||||
var savedTootUid: Int? = null,
|
||||
var tootText: String? = null,
|
||||
var mediaUrls: List<String>? = null,
|
||||
|
@ -1110,6 +1119,7 @@ class ComposeActivity : BaseActivity(),
|
|||
var scheduledAt: String? = null,
|
||||
var sensitive: Boolean? = null,
|
||||
var poll: NewPoll? = null,
|
||||
var modifiedInitialState: Boolean? = null,
|
||||
var tootRightNow: Boolean? = null
|
||||
) : Parcelable
|
||||
|
||||
|
@ -1119,7 +1129,7 @@ class ComposeActivity : BaseActivity(),
|
|||
private const val MEDIA_TAKE_PHOTO_RESULT = 2
|
||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
|
||||
private const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||
|
||||
// Mastodon only counts URLs as this long in terms of status character limits
|
||||
|
|
|
@ -19,8 +19,10 @@ import android.net.Uri
|
|||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.impl.utils.LiveDataUtils
|
||||
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
|
@ -33,6 +35,8 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Observable.empty
|
||||
import io.reactivex.Observable.just
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import java.util.*
|
||||
|
@ -58,6 +62,7 @@ class ComposeViewModel
|
|||
private var replyingStatusContent: String? = null
|
||||
internal var startingText: String? = null
|
||||
private var savedTootUid: Int = 0
|
||||
private var scheduledTootUid: String? = null
|
||||
private var startingContentWarning: String = ""
|
||||
private var inReplyToId: String? = null
|
||||
private var quoteId: String? = null
|
||||
|
@ -66,6 +71,7 @@ class ComposeViewModel
|
|||
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||
|
||||
private var contentWarningStateChanged: Boolean = false
|
||||
private var modifiedInitialState: Boolean = false
|
||||
|
||||
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
|
||||
|
||||
|
@ -98,6 +104,7 @@ class ComposeViewModel
|
|||
|
||||
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
||||
|
||||
private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty()
|
||||
|
||||
fun loadInstanceDataFromNetwork() {
|
||||
|
||||
|
@ -214,7 +221,7 @@ class ComposeViewModel
|
|||
val mediaChanged = !media.value.isNullOrEmpty()
|
||||
val pollChanged = poll.value != null
|
||||
|
||||
return textChanged || contentWarningChanged || mediaChanged || pollChanged
|
||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged
|
||||
}
|
||||
|
||||
fun contentWarningChanged(value: Boolean) {
|
||||
|
@ -257,7 +264,14 @@ class ComposeViewModel
|
|||
content: String,
|
||||
spoilerText: String
|
||||
): LiveData<Unit> {
|
||||
return media
|
||||
|
||||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit }
|
||||
} else {
|
||||
just(Unit)
|
||||
}.toLiveData()
|
||||
|
||||
val sendObservable = media
|
||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||
.map {
|
||||
val mediaIds = ArrayList<String>()
|
||||
|
@ -289,8 +303,13 @@ class ComposeViewModel
|
|||
idempotencyKey = randomAlphanumericString(16),
|
||||
retries = 0
|
||||
)
|
||||
|
||||
serviceClient.sendToot(tootToSend)
|
||||
}
|
||||
|
||||
return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit }
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
|
||||
|
@ -387,6 +406,7 @@ class ComposeViewModel
|
|||
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
||||
|
||||
inReplyToId = composeOptions?.inReplyToId
|
||||
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
||||
|
||||
quoteId = composeOptions?.quoteId
|
||||
quoteStatusAuthor = composeOptions?.quoteStatusAuthor
|
||||
|
@ -426,6 +446,7 @@ class ComposeViewModel
|
|||
|
||||
|
||||
savedTootUid = composeOptions?.savedTootUid ?: 0
|
||||
scheduledTootUid = composeOptions?.scheduledTootUid
|
||||
startingText = composeOptions?.tootText
|
||||
|
||||
|
||||
|
@ -490,4 +511,4 @@ data class ComposeInstanceParams(
|
|||
val pollMaxOptions: Int,
|
||||
val pollMaxLength: Int,
|
||||
val supportsScheduled: Boolean
|
||||
)
|
||||
)
|
||||
|
|
|
@ -105,7 +105,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
setupButtons(listener, account.getId(), status.getContent().toString(),
|
||||
false, account.getUsername(), statusDisplayOptions);
|
||||
false, statusDisplayOptions);
|
||||
|
||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
status.getMentions(), status.getEmojis(),
|
||||
|
@ -165,4 +165,4 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -37,7 +35,10 @@ import com.keylesspalace.tusky.di.ViewModelFactory
|
|||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import kotlinx.android.synthetic.main.fragment_timeline.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -85,21 +86,21 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
|
||||
initSwipeToRefresh()
|
||||
|
||||
viewModel.conversations.observe(viewLifecycleOwner, Observer<PagedList<ConversationEntity>> {
|
||||
viewModel.conversations.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
viewModel.networkState.observe(viewLifecycleOwner, Observer {
|
||||
}
|
||||
viewModel.networkState.observe(viewLifecycleOwner) {
|
||||
adapter.setNetworkState(it)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.load()
|
||||
|
||||
}
|
||||
|
||||
private fun initSwipeToRefresh() {
|
||||
viewModel.refreshState.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.refreshState.observe(viewLifecycleOwner) {
|
||||
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
|
||||
})
|
||||
}
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Marker
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isLessThan
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationFetcher @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val notifier: Notifier
|
||||
) {
|
||||
fun fetchAndShow() {
|
||||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||
if (account.notificationsEnabled) {
|
||||
try {
|
||||
val notifications = fetchNotifications(account)
|
||||
notifications.forEachIndexed { index, notification ->
|
||||
notifier.show(notification, account, index == 0)
|
||||
}
|
||||
accountManager.saveAccount(account)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error while fetching notifications", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNotifications(account: AccountEntity): MutableList<Notification> {
|
||||
val authHeader = String.format("Bearer %s", account.accessToken)
|
||||
// We fetch marker to not load/show notifications which user has already seen
|
||||
val marker = fetchMarker(authHeader, account)
|
||||
if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) {
|
||||
account.lastNotificationId = marker.lastReadId
|
||||
}
|
||||
Log.d(TAG, "getting Notifications for " + account.fullName)
|
||||
val notifications = mastodonApi.notificationsWithAuth(
|
||||
authHeader,
|
||||
account.domain,
|
||||
account.lastNotificationId
|
||||
).blockingGet()
|
||||
|
||||
val newId = account.lastNotificationId
|
||||
var newestId = ""
|
||||
val result = mutableListOf<Notification>()
|
||||
for (notification in notifications.reversed()) {
|
||||
val currentId = notification.id
|
||||
if (newestId.isLessThan(currentId)) {
|
||||
newestId = currentId
|
||||
account.lastNotificationId = currentId
|
||||
}
|
||||
if (newId.isLessThan(currentId)) {
|
||||
result.add(notification)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
|
||||
return try {
|
||||
val allMarkers = mastodonApi.markersWithAuth(
|
||||
authHeader,
|
||||
account.domain,
|
||||
listOf("notifications")
|
||||
).blockingGet()
|
||||
val notificationMarker = allMarkers["notifications"]
|
||||
Log.d(TAG, "Fetched marker: $notificationMarker")
|
||||
notificationMarker
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to fetch marker", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NotificationFetcher"
|
||||
}
|
||||
}
|
|
@ -37,7 +37,6 @@ import androidx.core.app.NotificationManagerCompat;
|
|||
import androidx.core.app.RemoteInput;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
|
@ -58,6 +57,7 @@ import com.keylesspalace.tusky.entity.PollOption;
|
|||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -145,7 +145,6 @@ public class NotificationHelper {
|
|||
|
||||
String rawCurrentNotifications = account.getActiveNotifications();
|
||||
JSONArray currentNotifications;
|
||||
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
|
||||
|
||||
try {
|
||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
||||
|
@ -174,7 +173,7 @@ public class NotificationHelper {
|
|||
|
||||
notificationId++;
|
||||
|
||||
builder.setContentTitle(titleForType(context, body, bidiFormatter, account))
|
||||
builder.setContentTitle(titleForType(context, body, account))
|
||||
.setContentText(bodyForType(body, context));
|
||||
|
||||
if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) {
|
||||
|
@ -243,7 +242,7 @@ public class NotificationHelper {
|
|||
if (currentNotifications.length() != 1) {
|
||||
try {
|
||||
String title = context.getString(R.string.notification_title_summary, currentNotifications.length());
|
||||
String text = joinNames(context, currentNotifications, bidiFormatter);
|
||||
String text = joinNames(context, currentNotifications);
|
||||
summaryBuilder.setContentTitle(title)
|
||||
.setContentText(text);
|
||||
} catch (JSONException e) {
|
||||
|
@ -573,36 +572,36 @@ public class NotificationHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private static String wrapItemAt(JSONArray array, int index, BidiFormatter bidiFormatter) throws JSONException {
|
||||
return bidiFormatter.unicodeWrap(array.get(index).toString());
|
||||
private static String wrapItemAt(JSONArray array, int index) throws JSONException {
|
||||
return StringUtils.unicodeWrap(array.get(index).toString());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String joinNames(Context context, JSONArray array, BidiFormatter bidiFormatter) throws JSONException {
|
||||
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
||||
if (array.length() > 3) {
|
||||
int length = array.length();
|
||||
return String.format(context.getString(R.string.notification_summary_large),
|
||||
wrapItemAt(array, length - 1, bidiFormatter),
|
||||
wrapItemAt(array, length - 2, bidiFormatter),
|
||||
wrapItemAt(array, length - 3, bidiFormatter),
|
||||
wrapItemAt(array, length - 1),
|
||||
wrapItemAt(array, length - 2),
|
||||
wrapItemAt(array, length - 3),
|
||||
length - 3);
|
||||
} else if (array.length() == 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_medium),
|
||||
wrapItemAt(array, 2, bidiFormatter),
|
||||
wrapItemAt(array, 1, bidiFormatter),
|
||||
wrapItemAt(array, 0, bidiFormatter));
|
||||
wrapItemAt(array, 2),
|
||||
wrapItemAt(array, 1),
|
||||
wrapItemAt(array, 0));
|
||||
} else if (array.length() == 2) {
|
||||
return String.format(context.getString(R.string.notification_summary_small),
|
||||
wrapItemAt(array, 1, bidiFormatter),
|
||||
wrapItemAt(array, 0, bidiFormatter));
|
||||
wrapItemAt(array, 1),
|
||||
wrapItemAt(array, 0));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String titleForType(Context context, Notification notification, BidiFormatter bidiFormatter, AccountEntity account) {
|
||||
String accountName = bidiFormatter.unicodeWrap(notification.getAccount().getName());
|
||||
private static String titleForType(Context context, Notification notification, AccountEntity account) {
|
||||
String accountName = StringUtils.unicodeWrap(notification.getAccount().getName());
|
||||
switch (notification.getType()) {
|
||||
case MENTION:
|
||||
return String.format(context.getString(R.string.notification_mention_format),
|
||||
|
|
|
@ -16,82 +16,35 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerFactory
|
||||
import androidx.work.WorkerParameters
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isLessThan
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationWorker(
|
||||
private val context: Context,
|
||||
context: Context,
|
||||
params: WorkerParameters,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
) : Worker(context, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
val accountList = accountManager.getAllAccountsOrderedByActive()
|
||||
for (account in accountList) {
|
||||
if (account.notificationsEnabled) {
|
||||
try {
|
||||
Log.d(TAG, "getting Notifications for " + account.fullName)
|
||||
val notificationsResponse = mastodonApi.notificationsWithAuth(
|
||||
String.format("Bearer %s", account.accessToken),
|
||||
account.domain
|
||||
).execute()
|
||||
val notifications = notificationsResponse.body()
|
||||
if (notificationsResponse.isSuccessful && notifications != null) {
|
||||
onNotificationsReceived(account, notifications)
|
||||
} else {
|
||||
Log.w(TAG, "error receiving notifications")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "error receiving notifications", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
notificationsFetcher.fetchAndShow()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun onNotificationsReceived(account: AccountEntity, notificationList: List<Notification>) {
|
||||
val newId = account.lastNotificationId
|
||||
var newestId = ""
|
||||
var isFirstOfBatch = true
|
||||
notificationList.reversed().forEach { notification ->
|
||||
val currentId = notification.id
|
||||
if (newestId.isLessThan(currentId)) {
|
||||
newestId = currentId
|
||||
}
|
||||
if (newId.isLessThan(currentId)) {
|
||||
NotificationHelper.make(context, notification, account, isFirstOfBatch)
|
||||
isFirstOfBatch = false
|
||||
}
|
||||
}
|
||||
account.lastNotificationId = newestId
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NotificationWorker"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NotificationWorkerFactory @Inject constructor(
|
||||
val api: MastodonApi,
|
||||
val accountManager: AccountManager
|
||||
): WorkerFactory() {
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
) : WorkerFactory() {
|
||||
|
||||
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
|
||||
if(workerClassName == NotificationWorker::class.java.name) {
|
||||
return NotificationWorker(appContext, workerParameters, api, accountManager)
|
||||
override fun createWorker(
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters
|
||||
): ListenableWorker? {
|
||||
if (workerClassName == NotificationWorker::class.java.name) {
|
||||
return NotificationWorker(appContext, workerParameters, notificationsFetcher)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
|
||||
/**
|
||||
* Shows notifications.
|
||||
*/
|
||||
interface Notifier {
|
||||
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
|
||||
}
|
||||
|
||||
class SystemNotifier(
|
||||
private val context: Context
|
||||
) : Notifier {
|
||||
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
|
||||
NotificationHelper.make(context, notification, account, isFirstInBatch)
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
|
||||
|
@ -77,7 +76,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.navigation.observe(this, Observer { screen ->
|
||||
viewModel.navigation.observe(this) { screen ->
|
||||
if (screen != null) {
|
||||
viewModel.navigated()
|
||||
when (screen) {
|
||||
|
@ -88,14 +87,14 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
Screen.Finish -> closeScreen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.checkUrl.observe(this, Observer {
|
||||
viewModel.checkUrl.observe(this) {
|
||||
if (!it.isNullOrBlank()) {
|
||||
viewModel.urlChecked()
|
||||
viewUrl(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPreviousScreen() {
|
||||
|
|
|
@ -19,6 +19,9 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
|
@ -31,6 +34,7 @@ import javax.inject.Inject
|
|||
|
||||
class ReportViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
|
||||
|
||||
private val navigationMutable = MutableLiveData<Screen>()
|
||||
|
@ -96,7 +100,7 @@ class ReportViewModel @Inject constructor(
|
|||
val ids = listOf(accountId)
|
||||
muteStateMutable.value = Loading()
|
||||
blockStateMutable.value = Loading()
|
||||
mastodonApi.relationshipsObservable(ids)
|
||||
mastodonApi.relationships(ids)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
|
@ -123,16 +127,21 @@ class ReportViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun toggleMute() {
|
||||
if (muteStateMutable.value?.data == true) {
|
||||
mastodonApi.unmuteAccountObservable(accountId)
|
||||
val alreadyMuted = muteStateMutable.value?.data == true
|
||||
if (alreadyMuted) {
|
||||
mastodonApi.unmuteAccount(accountId)
|
||||
} else {
|
||||
mastodonApi.muteAccountObservable(accountId)
|
||||
mastodonApi.muteAccount(accountId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ relationship ->
|
||||
muteStateMutable.value = Success(relationship?.muting == true)
|
||||
val muting = relationship?.muting == true
|
||||
muteStateMutable.value = Success(muting)
|
||||
if (muting) {
|
||||
eventHub.dispatch(MuteEvent(accountId))
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
muteStateMutable.value = Error(false, error.message)
|
||||
|
@ -143,16 +152,21 @@ class ReportViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun toggleBlock() {
|
||||
if (blockStateMutable.value?.data == true) {
|
||||
mastodonApi.unblockAccountObservable(accountId)
|
||||
val alreadyBlocked = blockStateMutable.value?.data == true
|
||||
if (alreadyBlocked) {
|
||||
mastodonApi.unblockAccount(accountId)
|
||||
} else {
|
||||
mastodonApi.blockAccountObservable(accountId)
|
||||
mastodonApi.blockAccount(accountId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ relationship ->
|
||||
blockStateMutable.value = Success(relationship?.blocking == true)
|
||||
val blocking = relationship?.blocking == true
|
||||
blockStateMutable.value = Success(blocking)
|
||||
if (blocking) {
|
||||
eventHub.dispatch(BlockEvent(accountId))
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
blockStateMutable.value = Error(false, error.message)
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
package com.keylesspalace.tusky.components.report.adapter
|
||||
|
||||
import android.view.View
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import java.util.ArrayList
|
||||
|
||||
interface AdapterHandler: LinkListener {
|
||||
fun showMedia(v: View?, status: Status?, idx: Int)
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.paging.toLiveData
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.BiListing
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
|
@ -55,7 +54,7 @@ class ReportDoneFragment : Fragment(), Injectable {
|
|||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.muteState.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.muteState.observe(viewLifecycleOwner) {
|
||||
if (it !is Loading) {
|
||||
buttonMute.show()
|
||||
progressMute.show()
|
||||
|
@ -68,9 +67,9 @@ class ReportDoneFragment : Fragment(), Injectable {
|
|||
true -> R.string.action_unmute
|
||||
else -> R.string.action_mute
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.blockState.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.blockState.observe(viewLifecycleOwner) {
|
||||
if (it !is Loading) {
|
||||
buttonBlock.show()
|
||||
progressBlock.show()
|
||||
|
@ -83,7 +82,7 @@ class ReportDoneFragment : Fragment(), Injectable {
|
|||
true -> R.string.action_unblock
|
||||
else -> R.string.action_block
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.view.ViewGroup
|
|||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
|
@ -81,14 +80,14 @@ class ReportNoteFragment : Fragment(), Injectable {
|
|||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
viewModel.reportingState.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.reportingState.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
is Success -> viewModel.navigateTo(Screen.Done)
|
||||
is Loading -> showLoading()
|
||||
is Error -> showError(it.cause)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(error: Throwable?) {
|
||||
|
|
|
@ -23,8 +23,6 @@ import androidx.core.app.ActivityOptionsCompat
|
|||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -44,7 +42,10 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import kotlinx.android.synthetic.main.fragment_report_statuses.*
|
||||
import javax.inject.Inject
|
||||
|
@ -131,11 +132,11 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
|||
recyclerView.adapter = adapter
|
||||
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
viewModel.statuses.observe(viewLifecycleOwner, Observer<PagedList<Status>> {
|
||||
viewModel.statuses.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.networkStateAfter.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
progressBarBottom.show()
|
||||
else
|
||||
|
@ -143,9 +144,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
|||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.networkStateBefore.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
|
||||
progressBarTop.show()
|
||||
else
|
||||
|
@ -153,9 +154,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
|||
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing)
|
||||
progressBarLoading.show()
|
||||
else
|
||||
|
@ -165,7 +166,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
|||
swipeRefreshLayout.isRefreshing = false
|
||||
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
|
||||
showError(it.msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
|
||||
|
|
|
@ -19,7 +19,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -30,7 +29,6 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
import com.keylesspalace.tusky.util.Status
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import kotlinx.android.synthetic.main.activity_scheduled_toot.*
|
||||
|
@ -68,11 +66,11 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
|
||||
viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java]
|
||||
|
||||
viewModel.data.observe(this, Observer {
|
||||
viewModel.data.observe(this) {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.networkState.observe(this, Observer { (status) ->
|
||||
viewModel.networkState.observe(this) { (status) ->
|
||||
when(status) {
|
||||
Status.SUCCESS -> {
|
||||
progressBar.hide()
|
||||
|
@ -103,9 +101,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -124,6 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
|||
|
||||
override fun edit(item: ScheduledStatus) {
|
||||
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
|
||||
scheduledTootUid = item.id,
|
||||
tootText = item.params.text,
|
||||
contentWarning = item.params.spoilerText,
|
||||
mediaAttachments = item.mediaAttachments,
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.view.ViewGroup
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
|
@ -61,11 +60,11 @@ abstract class SearchFragment<T> : Fragment(),
|
|||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
data.observe(viewLifecycleOwner, Observer {
|
||||
data.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
}
|
||||
|
||||
networkStateRefresh.observe(viewLifecycleOwner, Observer {
|
||||
networkStateRefresh.observe(viewLifecycleOwner) {
|
||||
|
||||
searchProgressBar.visible(it == NetworkState.LOADING)
|
||||
|
||||
|
@ -73,17 +72,16 @@ abstract class SearchFragment<T> : Fragment(),
|
|||
showError()
|
||||
}
|
||||
checkNoData()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
networkState.observe(viewLifecycleOwner, Observer {
|
||||
networkState.observe(viewLifecycleOwner) {
|
||||
|
||||
progressBarBottom.visible(it == NetworkState.LOADING)
|
||||
|
||||
if (it.status == Status.FAILED) {
|
||||
showError()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkNoData() {
|
||||
|
|
|
@ -26,8 +26,6 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
|
@ -403,9 +401,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
private fun onMute(accountId: String, accountUsername: String) {
|
||||
showMuteAccountDialog(
|
||||
this.requireActivity(),
|
||||
accountUsername,
|
||||
{ notifications -> viewModel.muteAccount(accountId, notifications) }
|
||||
)
|
||||
accountUsername
|
||||
) { notifications ->
|
||||
viewModel.muteAccount(accountId, notifications)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accountIsInMentions(account: AccountEntity?, mentions: Array<Mention>): Boolean {
|
||||
|
|
|
@ -21,7 +21,6 @@ import com.keylesspalace.tusky.entity.Status
|
|||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.Comparator
|
||||
|
||||
/**
|
||||
* This class caches the account database and handles all account related operations
|
||||
|
@ -166,13 +165,13 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
*/
|
||||
fun getAllAccountsOrderedByActive(): List<AccountEntity> {
|
||||
val accountsCopy = accounts.toMutableList()
|
||||
accountsCopy.sortWith(Comparator { l, r ->
|
||||
accountsCopy.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return accountsCopy
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||
|
@ -105,6 +106,9 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesAccessTokenLoginActivity(): AccessTokenLoginActivity
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import androidx.room.Room
|
|||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.EventHubImpl
|
||||
import com.keylesspalace.tusky.components.notifications.Notifier
|
||||
import com.keylesspalace.tusky.components.notifications.SystemNotifier
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
|
@ -79,7 +81,11 @@ class AppModule {
|
|||
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)
|
||||
.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notifier(context: Context): Notifier = SystemNotifier(context)
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
|
@ -86,6 +87,11 @@ abstract class ViewModelModule {
|
|||
@ViewModelKey(ScheduledTootViewModel::class)
|
||||
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(AnnouncementsViewModel::class)
|
||||
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(QuickTootViewModel::class)
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/* Copyright 2020 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.text.Spanned
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.*
|
||||
|
||||
data class Announcement(
|
||||
val id: String,
|
||||
val content: Spanned,
|
||||
@SerializedName("starts_at") val startsAt: Date?,
|
||||
@SerializedName("ends_at") val endsAt: Date?,
|
||||
@SerializedName("all_day") val allDay: Boolean,
|
||||
@SerializedName("published_at") val publishedAt: Date,
|
||||
@SerializedName("updated_at") val updatedAt: Date,
|
||||
val read: Boolean,
|
||||
val mentions: List<Status.Mention>,
|
||||
val statuses: List<Status>,
|
||||
val tags: List<HashTag>,
|
||||
val emojis: List<Emoji>,
|
||||
val reactions: List<Reaction>
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val announcement = other as Announcement?
|
||||
return id == announcement?.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
data class Reaction(
|
||||
val name: String,
|
||||
var count: Int,
|
||||
var me: Boolean,
|
||||
val url: String?,
|
||||
@SerializedName("static_url") val staticUrl: String?
|
||||
)
|
||||
}
|
|
@ -26,7 +26,9 @@ data class Card(
|
|||
val image: String,
|
||||
val type: String,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
val height: Int,
|
||||
val blurhash: String?,
|
||||
val embed_url: String?
|
||||
) {
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -41,4 +43,7 @@ data class Card(
|
|||
return account?.url == this.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_PHOTO = "photo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,5 +23,6 @@ import kotlinx.android.parcel.Parcelize
|
|||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val url: String,
|
||||
@SerializedName("static_url") val staticUrl: String,
|
||||
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
|
||||
) : Parcelable
|
||||
) : Parcelable
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* API type for saving the scroll position of a timeline.
|
||||
*/
|
||||
data class Marker(
|
||||
@SerializedName("last_read_id")
|
||||
val lastReadId: String,
|
||||
val version: Int,
|
||||
@SerializedName("updated_at")
|
||||
val updatedAt: Date
|
||||
)
|
|
@ -26,5 +26,6 @@ data class Relationship (
|
|||
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
|
||||
val requested: Boolean,
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
val note: String? // nullable for backward compatibility / feature detection
|
||||
)
|
||||
|
|
|
@ -52,7 +52,6 @@ import java.io.IOException
|
|||
import java.util.HashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
||||
|
||||
@Inject
|
||||
|
@ -116,27 +115,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
}
|
||||
|
||||
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
|
||||
val callback = object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
|
||||
if (response.isSuccessful) {
|
||||
onMuteSuccess(mute, id, position, notifications)
|
||||
} else {
|
||||
onMuteFailure(mute, id, notifications)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {
|
||||
onMuteFailure(mute, id, notifications)
|
||||
}
|
||||
}
|
||||
|
||||
val call = if (!mute) {
|
||||
if (!mute) {
|
||||
api.unmuteAccount(id)
|
||||
} else {
|
||||
api.muteAccount(id, notifications)
|
||||
}
|
||||
callList.add(call)
|
||||
call.enqueue(callback)
|
||||
.autoDispose(from(this))
|
||||
.subscribe({
|
||||
onMuteSuccess(mute, id, position, notifications)
|
||||
}, {
|
||||
onMuteFailure(mute, id, notifications)
|
||||
})
|
||||
}
|
||||
|
||||
private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) {
|
||||
|
@ -171,27 +160,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
}
|
||||
|
||||
override fun onBlock(block: Boolean, id: String, position: Int) {
|
||||
val cb = object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
|
||||
if (response.isSuccessful) {
|
||||
onBlockSuccess(block, id, position)
|
||||
} else {
|
||||
onBlockFailure(block, id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {
|
||||
onBlockFailure(block, id)
|
||||
}
|
||||
}
|
||||
|
||||
val call = if (!block) {
|
||||
if (!block) {
|
||||
api.unblockAccount(id)
|
||||
} else {
|
||||
api.blockAccount(id)
|
||||
}
|
||||
callList.add(call)
|
||||
call.enqueue(cb)
|
||||
.autoDispose(from(this))
|
||||
.subscribe({
|
||||
onBlockSuccess(block, id, position)
|
||||
}, {
|
||||
onBlockFailure(block, id)
|
||||
})
|
||||
}
|
||||
|
||||
private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) {
|
||||
|
@ -350,29 +329,16 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
}
|
||||
|
||||
private fun fetchRelationships(ids: List<String>) {
|
||||
val callback = object : Callback<List<Relationship>> {
|
||||
override fun onResponse(call: Call<List<Relationship>>, response: Response<List<Relationship>>) {
|
||||
val body = response.body()
|
||||
if (response.isSuccessful && body != null) {
|
||||
onFetchRelationshipsSuccess(body)
|
||||
} else {
|
||||
api.relationships(ids)
|
||||
.autoDispose(from(this))
|
||||
.subscribe(::onFetchRelationshipsSuccess) {
|
||||
onFetchRelationshipsFailure(ids)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
||||
onFetchRelationshipsFailure(ids)
|
||||
}
|
||||
}
|
||||
|
||||
val call = api.relationships(ids)
|
||||
callList.add(call)
|
||||
call.enqueue(callback)
|
||||
}
|
||||
|
||||
private fun onFetchRelationshipsSuccess(relationships: List<Relationship>) {
|
||||
val mutesAdapter = adapter as MutesAdapter
|
||||
var mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) }
|
||||
mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap)
|
||||
}
|
||||
|
|
|
@ -484,6 +484,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||
composeOptions.setMediaAttachments(deletedStatus.getAttachments());
|
||||
composeOptions.setSensitive(deletedStatus.getSensitive());
|
||||
composeOptions.setModifiedInitialState(true);
|
||||
if (deletedStatus.getPoll() != null) {
|
||||
composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
|
||||
}
|
||||
|
|
|
@ -1355,7 +1355,7 @@ public class TimelineFragment extends SFragment implements
|
|||
if (status != null
|
||||
&& ((status.getInReplyToId() != null && filterRemoveReplies)
|
||||
|| (status.getReblog() != null && filterRemoveReblogs)
|
||||
|| shouldFilterStatus(status))) {
|
||||
|| shouldFilterStatus(status.getActionableStatus()))) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,9 +99,9 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
url = attachment.url
|
||||
description = attachment.description
|
||||
} else {
|
||||
url = arguments.getString(ARG_AVATAR_URL)
|
||||
url = arguments.getString(ARG_SINGLE_IMAGE_URL)
|
||||
if (url == null) {
|
||||
throw IllegalArgumentException("attachment or avatar url has to be set")
|
||||
throw IllegalArgumentException("attachment or image url has to be set")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +158,9 @@ class ViewImageFragment : ViewMediaFragment() {
|
|||
}
|
||||
|
||||
private fun onGestureEnd() {
|
||||
if (photoView == null) {
|
||||
return
|
||||
}
|
||||
if (abs(photoView.translationY) > 180) {
|
||||
photoActionsListener.onDismiss()
|
||||
} else {
|
||||
|
|
|
@ -42,7 +42,7 @@ abstract class ViewMediaFragment : BaseFragment() {
|
|||
@JvmStatic
|
||||
protected val ARG_ATTACHMENT = "attach"
|
||||
@JvmStatic
|
||||
protected val ARG_AVATAR_URL = "avatarUrl"
|
||||
protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
|
||||
|
@ -62,10 +62,10 @@ abstract class ViewMediaFragment : BaseFragment() {
|
|||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newAvatarInstance(avatarUrl: String): ViewMediaFragment {
|
||||
fun newSingleImageInstance(imageUrl: String): ViewMediaFragment {
|
||||
val arguments = Bundle(2)
|
||||
val fragment = ViewImageFragment()
|
||||
arguments.putString(ARG_AVATAR_URL, avatarUrl)
|
||||
arguments.putString(ARG_SINGLE_IMAGE_URL, imageUrl)
|
||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true)
|
||||
|
||||
fragment.arguments = arguments
|
||||
|
|
|
@ -99,11 +99,19 @@ interface MastodonApi {
|
|||
@Query("exclude_types[]") excludes: Set<Notification.Type>?
|
||||
): Call<List<Notification>>
|
||||
|
||||
@GET("api/v1/markers")
|
||||
fun markersWithAuth(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Query("timeline[]") timelines: List<String>
|
||||
): Single<Map<String, Marker>>
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
fun notificationsWithAuth(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String
|
||||
): Call<List<Notification>>
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Query("since_id") sinceId: String?
|
||||
): Single<List<Notification>>
|
||||
|
||||
@POST("api/v1/notifications/clear")
|
||||
fun clearNotifications(): Call<ResponseBody>
|
||||
|
@ -261,7 +269,7 @@ interface MastodonApi {
|
|||
@GET("api/v1/accounts/{id}")
|
||||
fun account(
|
||||
@Path("id") accountId: String
|
||||
): Call<Account>
|
||||
): Single<Account>
|
||||
|
||||
/**
|
||||
* Method to fetch statuses for the specified account.
|
||||
|
@ -300,44 +308,44 @@ interface MastodonApi {
|
|||
fun followAccount(
|
||||
@Path("id") accountId: String,
|
||||
@Field("reblogs") showReblogs: Boolean
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
fun unfollowAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccount(
|
||||
@Path("id") accountId: String,
|
||||
@Field("notifications") notifications: Boolean
|
||||
): Call<Relationship>
|
||||
@Field("notifications") notifications: Boolean? = null
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationships(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Call<List<Relationship>>
|
||||
): Single<List<Relationship>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/identity_proofs")
|
||||
fun identityProofs(
|
||||
@Path("id") accountId: String
|
||||
): Call<List<IdentityProof>>
|
||||
): Single<List<IdentityProof>>
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
fun blocks(
|
||||
|
@ -505,30 +513,27 @@ interface MastodonApi {
|
|||
@Field("choices[]") choices: List<Int>
|
||||
): Single<Poll>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@GET("api/v1/announcements")
|
||||
fun listAnnouncements(
|
||||
@Query("with_dismissed") withDismissed: Boolean = true
|
||||
): Single<List<Announcement>>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@POST("api/v1/announcements/{id}/dismiss")
|
||||
fun dismissAnnouncement(
|
||||
@Path("id") announcementId: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
||||
fun addAnnouncementReaction(
|
||||
@Path("id") announcementId: String,
|
||||
@Path("name") name: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationshipsObservable(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Single<List<Relationship>>
|
||||
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
||||
fun removeAnnouncementReaction(
|
||||
@Path("id") announcementId: String,
|
||||
@Path("name") name: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
|
@ -563,4 +568,11 @@ interface MastodonApi {
|
|||
@Query("following") following: Boolean? = null
|
||||
): Single<SearchResult>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/note")
|
||||
fun updateAccountNote(
|
||||
@Path("id") accountId: String,
|
||||
@Field("comment") note: String
|
||||
): Single<Relationship>
|
||||
|
||||
}
|
||||
|
|
|
@ -15,17 +15,14 @@
|
|||
|
||||
package com.keylesspalace.tusky.network
|
||||
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
|
@ -108,24 +105,23 @@ class TimelineCasesImpl(
|
|||
}
|
||||
|
||||
override fun mute(id: String, notifications: Boolean) {
|
||||
val call = mastodonApi.muteAccount(id, notifications)
|
||||
call.enqueue(object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
||||
})
|
||||
eventHub.dispatch(MuteEvent(id))
|
||||
mastodonApi.muteAccount(id, notifications)
|
||||
.subscribe({
|
||||
eventHub.dispatch(MuteEvent(id))
|
||||
}, { t ->
|
||||
Log.w("Failed to mute account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun block(id: String) {
|
||||
val call = mastodonApi.blockAccount(id)
|
||||
call.enqueue(object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
||||
})
|
||||
eventHub.dispatch(BlockEvent(id))
|
||||
|
||||
mastodonApi.blockAccount(id)
|
||||
.subscribe({
|
||||
eventHub.dispatch(BlockEvent(id))
|
||||
}, { t ->
|
||||
Log.w("Failed to block account", t)
|
||||
})
|
||||
.addTo(cancelDisposable)
|
||||
}
|
||||
|
||||
override fun delete(id: String): Single<DeletedStatus> {
|
||||
|
|
|
@ -5,14 +5,14 @@ import androidx.fragment.app.FragmentActivity
|
|||
import com.keylesspalace.tusky.ViewMediaAdapter
|
||||
import com.keylesspalace.tusky.fragment.ViewMediaFragment
|
||||
|
||||
class AvatarImagePagerAdapter(
|
||||
class SingleImagePagerAdapter(
|
||||
activity: FragmentActivity,
|
||||
private val avatarUrl: String
|
||||
private val imageUrl: String
|
||||
) : ViewMediaAdapter(activity) {
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return if (position == 0) {
|
||||
ViewMediaFragment.newAvatarInstance(avatarUrl)
|
||||
ViewMediaFragment.newSingleImageInstance(imageUrl)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
|
@ -143,9 +143,9 @@ class EmojiCompatFont(
|
|||
listOf(0)
|
||||
}
|
||||
Pair(file, versionCode)
|
||||
}.sortedWith(
|
||||
Comparator<Pair<File, List<Int>>> { a, b -> compareVersions(a.second, b.second) }
|
||||
).also {
|
||||
}.sortedWith { a, b ->
|
||||
compareVersions(a.second, b.second)
|
||||
}.also {
|
||||
existingFontFileCache = it
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,8 +285,7 @@ class StatusViewHelper(private val itemView: View) {
|
|||
if (useAbsoluteTime) {
|
||||
context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
|
||||
} else {
|
||||
val pollDuration = TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
||||
context.getString(R.string.poll_info_time_relative, pollDuration)
|
||||
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,3 +80,12 @@ fun Spanned.trimTrailingWhitespace(): Spanned {
|
|||
} while (i >= 0 && get(i).isWhitespace())
|
||||
return subSequence(0, i + 1) as Spanned
|
||||
}
|
||||
|
||||
/**
|
||||
* BidiFormatter.unicodeWrap is insufficient in some cases (see #1921)
|
||||
* So we force isolation manually
|
||||
* https://unicode.org/reports/tr9/#Explicit_Directional_Isolates
|
||||
*/
|
||||
fun CharSequence.unicodeWrap(): String {
|
||||
return "\u2068${this}\u2069"
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.keylesspalace.tusky.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class EmojiPicker @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : RecyclerView(context, attrs) {
|
||||
|
||||
init {
|
||||
clipToPadding = false
|
||||
layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ fun showMuteAccountDialog(
|
|||
(view.findViewById(R.id.warning) as TextView).text =
|
||||
activity.getString(R.string.dialog_mute_warning, accountUsername)
|
||||
val checkbox: CheckBox = view.findViewById(R.id.checkbox)
|
||||
checkbox.setChecked(true)
|
||||
checkbox.isChecked = true
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
.setView(view)
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.keylesspalace.tusky.viewmodel
|
|||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
|
@ -11,70 +10,66 @@ import com.keylesspalace.tusky.entity.IdentityProof
|
|||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val accountManager: AccountManager
|
||||
) : ViewModel() {
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||
|
||||
val noteSaved = MutableLiveData<Boolean>()
|
||||
|
||||
private val identityProofData = MutableLiveData<List<IdentityProof>>()
|
||||
|
||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right<IdentityProof, Field>(it) })
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
}
|
||||
|
||||
|
||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
||||
private val disposable: Disposable = eventHub.events
|
||||
.subscribe { event ->
|
||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
||||
accountData.postValue(Success(event.newProfileData))
|
||||
}
|
||||
}
|
||||
|
||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
||||
private var isDataLoading = false
|
||||
|
||||
lateinit var accountId: String
|
||||
var isSelf = false
|
||||
|
||||
private var noteDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
eventHub.events
|
||||
.subscribe { event ->
|
||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
||||
accountData.postValue(Success(event.newProfileData))
|
||||
}
|
||||
}.autoDispose()
|
||||
}
|
||||
|
||||
private fun obtainAccount(reload: Boolean = false) {
|
||||
if (accountData.value == null || reload) {
|
||||
isDataLoading = true
|
||||
accountData.postValue(Loading())
|
||||
|
||||
val call = mastodonApi.account(accountId)
|
||||
call.enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>,
|
||||
response: Response<Account>) {
|
||||
if (response.isSuccessful) {
|
||||
accountData.postValue(Success(response.body()))
|
||||
} else {
|
||||
mastodonApi.account(accountId)
|
||||
.subscribe({ account ->
|
||||
accountData.postValue(Success(account))
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
}, {t ->
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
accountData.postValue(Error())
|
||||
}
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
accountData.postValue(Error())
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,51 +78,27 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
relationshipData.postValue(Loading())
|
||||
|
||||
val ids = listOf(accountId)
|
||||
val call = mastodonApi.relationships(ids)
|
||||
call.enqueue(object : Callback<List<Relationship>> {
|
||||
override fun onResponse(call: Call<List<Relationship>>,
|
||||
response: Response<List<Relationship>>) {
|
||||
val relationships = response.body()
|
||||
if (response.isSuccessful && relationships != null && relationships.getOrNull(0) != null) {
|
||||
val relationship = relationships[0]
|
||||
relationshipData.postValue(Success(relationship))
|
||||
} else {
|
||||
mastodonApi.relationships(listOf(accountId))
|
||||
.subscribe({ relationships ->
|
||||
relationshipData.postValue(Success(relationships[0]))
|
||||
}, { t ->
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
relationshipData.postValue(Error())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
relationshipData.postValue(Error())
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun obtainIdentityProof(reload: Boolean = false) {
|
||||
if (identityProofData.value == null || reload) {
|
||||
|
||||
val call = mastodonApi.identityProofs(accountId)
|
||||
call.enqueue(object : Callback<List<IdentityProof>> {
|
||||
override fun onResponse(call: Call<List<IdentityProof>>,
|
||||
response: Response<List<IdentityProof>>) {
|
||||
val proofs = response.body()
|
||||
if (response.isSuccessful && proofs != null ) {
|
||||
mastodonApi.identityProofs(accountId)
|
||||
.subscribe({ proofs ->
|
||||
identityProofData.postValue(proofs)
|
||||
} else {
|
||||
identityProofData.postValue(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<IdentityProof>>, t: Throwable) {
|
||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
}, { t ->
|
||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,11 +201,15 @@ class AccountViewModel @Inject constructor(
|
|||
relationshipData.postValue(Loading(newRelation))
|
||||
}
|
||||
|
||||
val callback = object : Callback<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>,
|
||||
response: Response<Relationship>) {
|
||||
val relationship = response.body()
|
||||
if (response.isSuccessful && relationship != null) {
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
|
||||
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
|
||||
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
|
||||
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
|
||||
}.subscribe(
|
||||
{ relationship ->
|
||||
relationshipData.postValue(Success(relationship))
|
||||
|
||||
when (relationshipAction) {
|
||||
|
@ -244,37 +219,35 @@ class AccountViewModel @Inject constructor(
|
|||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
},
|
||||
{
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
}
|
||||
|
||||
val call = when (relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
|
||||
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
|
||||
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
|
||||
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
|
||||
}
|
||||
|
||||
call.enqueue(callback)
|
||||
callList.add(call)
|
||||
|
||||
fun noteChanged(newNote: String) {
|
||||
noteSaved.postValue(false)
|
||||
noteDisposable?.dispose()
|
||||
noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS)
|
||||
.flatMap {
|
||||
mastodonApi.updateAccountNote(accountId, newNote)
|
||||
}
|
||||
.doOnSuccess {
|
||||
noteSaved.postValue(true)
|
||||
}
|
||||
.delay(4, TimeUnit.SECONDS)
|
||||
.subscribe({
|
||||
noteSaved.postValue(false)
|
||||
}, {
|
||||
Log.e(TAG, "Error updating note", it)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
callList.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
disposable.dispose()
|
||||
super.onCleared()
|
||||
noteDisposable?.dispose()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,8H4A2,2 0,0 0,2 10V14A2,2 0,0 0,4 16H5V20A1,1 0,0 0,6 21H8A1,1 0,0 0,9 20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" />
|
||||
</vector>
|
|
@ -168,16 +168,43 @@
|
|||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/accountNoteTextInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/account_note_hint" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/saveNoteInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account_note_saved"
|
||||
android:textColor="@color/tusky_blue"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/accountNoteTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingTop="2dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/saveNoteInfo"
|
||||
tools:text="This is a test description. Descriptions can be quite looooong." />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -244,6 +271,7 @@
|
|||
android:text="@string/title_statuses"
|
||||
android:textColor="@color/account_tab_font_color"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/announcementsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||
android:id="@+id/errorMessageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@android:color/transparent"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/elephant_error"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -251,14 +251,12 @@
|
|||
android:textSize="?attr/status_text_medium" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<com.keylesspalace.tusky.view.EmojiPicker
|
||||
android:id="@+id/emojiView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="12dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
|
@ -385,6 +383,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="bold"
|
||||
tools:text="500" />
|
||||
|
||||
<com.keylesspalace.tusky.components.compose.view.TootButton
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tabPreferenceContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -19,8 +20,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/scrimBackground"
|
||||
android:visibility="invisible"
|
||||
app:layout_behavior="@string/fab_transformation_scrim_behavior" />
|
||||
android:visibility="invisible" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/actionButton"
|
||||
|
@ -30,7 +30,7 @@
|
|||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_plus_24dp" />
|
||||
|
||||
<com.google.android.material.transformation.TransformationChildCard
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/sheet"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -38,8 +38,7 @@
|
|||
android:layout_margin="16dp"
|
||||
android:visibility="invisible"
|
||||
app:cardBackgroundColor="?attr/colorSurface"
|
||||
app:cardElevation="2dp"
|
||||
app:layout_behavior="@string/fab_transformation_sheet_behavior">
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="240dp"
|
||||
|
@ -76,6 +75,7 @@
|
|||
android:textColor="?attr/colorOnPrimary"
|
||||
android:textSize="?attr/status_text_large" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.transformation.TransformationChildCard>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:padding="8dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/addReactionChip"
|
||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkable="false"
|
||||
app:chipEndPadding="4dp"
|
||||
app:chipIcon="@drawable/ic_plus_24dp"
|
||||
app:chipSurfaceColor="@color/tusky_blue"
|
||||
app:textEndPadding="0dp"
|
||||
app:textStartPadding="0dp" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -20,7 +20,7 @@
|
|||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="28dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -489,6 +489,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_poll_button"
|
||||
|
|
|
@ -349,7 +349,6 @@
|
|||
<string name="compose_shortcut_long_label">تحرير تبويق</string>
|
||||
<string name="compose_shortcut_short_label">كتابة</string>
|
||||
<string name="notification_clear_text">هل تريد حقا مسح كافة إشعاراتك؟</string>
|
||||
<string name="poll_info_time_relative">%s متبقي</string>
|
||||
<string name="poll_info_time_absolute">ينتهي في %s</string>
|
||||
<string name="poll_info_closed">انتهى</string>
|
||||
<string name="poll_vote">صَوِّت</string>
|
||||
|
@ -384,36 +383,36 @@
|
|||
</plurals>
|
||||
<string name="pref_title_bot_overlay">إظهار علامة البوتات</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="zero">%d أيام</item>
|
||||
<item quantity="one">%d يوم</item>
|
||||
<item quantity="two">%d يومين</item>
|
||||
<item quantity="few">%d أيام</item>
|
||||
<item quantity="many">%d أيام</item>
|
||||
<item quantity="other">%d أيام</item>
|
||||
<item quantity="zero">%d أيام متبقية</item>
|
||||
<item quantity="one">%d يوم متبقي</item>
|
||||
<item quantity="two">%d يومين متبقيين</item>
|
||||
<item quantity="few">%d أيام متبقية</item>
|
||||
<item quantity="many">%d أيام متبقية</item>
|
||||
<item quantity="other">%d أيام متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="zero">%d ساعات</item>
|
||||
<item quantity="one">%d ساعة</item>
|
||||
<item quantity="two">%d ساعتين</item>
|
||||
<item quantity="few">%d ساعات</item>
|
||||
<item quantity="many">%d ساعات</item>
|
||||
<item quantity="other">%d ساعات</item>
|
||||
<item quantity="zero">%d ساعات متبقية</item>
|
||||
<item quantity="one">%d ساعة متبقية</item>
|
||||
<item quantity="two">%d ساعتان متبقيتان</item>
|
||||
<item quantity="few">%d ساعات متبقية</item>
|
||||
<item quantity="many">%d ساعات متبقية</item>
|
||||
<item quantity="other">%d ساعات متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="zero">%d دقائق</item>
|
||||
<item quantity="one">%d دقيقة</item>
|
||||
<item quantity="two">%d دقيقتين</item>
|
||||
<item quantity="few">%d دقائق</item>
|
||||
<item quantity="many">%d دقائق</item>
|
||||
<item quantity="other">%d دقائق</item>
|
||||
<item quantity="zero">%d دقائق متبقية</item>
|
||||
<item quantity="one">%d دقيقة متبقية</item>
|
||||
<item quantity="two">%d دقيقتان متبقيتان</item>
|
||||
<item quantity="few">%d دقائق متبقية</item>
|
||||
<item quantity="many">%d دقائق متبقية</item>
|
||||
<item quantity="other">%d دقائق متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="zero">%d ثوان</item>
|
||||
<item quantity="one">%d ثانية</item>
|
||||
<item quantity="two">%d ثانيتين</item>
|
||||
<item quantity="few">%d ثوان</item>
|
||||
<item quantity="many">%d ثوان</item>
|
||||
<item quantity="other">%d ثوان</item>
|
||||
<item quantity="zero">%d ثوان متبقية</item>
|
||||
<item quantity="one">%d ثانية متبقية</item>
|
||||
<item quantity="two">%d ثانيتان متبقيتان</item>
|
||||
<item quantity="few">%d ثوان متبقية</item>
|
||||
<item quantity="many">%d ثوان متبقية</item>
|
||||
<item quantity="other">%d ثوان متبقية</item>
|
||||
</plurals>
|
||||
<string name="compose_preview_image_description">إجراءات على الصورة %s</string>
|
||||
<string name="caption_notoemoji">حزمة الإيموجي الحالية لـ غوغل</string>
|
||||
|
|
|
@ -366,7 +366,6 @@
|
|||
<string name="notification_clear_text">আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\?</string>
|
||||
<string name="compose_preview_image_description">ছবি %s এর জন্য ক্রিয়া</string>
|
||||
<string name="poll_info_format"> <!-- ১৫ ভোট • ১ ঘন্টা বাকি --> %1$s • %2$s</string>
|
||||
<string name="poll_info_time_relative">%s বাকি</string>
|
||||
<string name="poll_info_time_absolute">%s এ শেষ হবে</string>
|
||||
<string name="poll_info_closed">বন্ধ</string>
|
||||
<string name="poll_vote">ভোট</string>
|
||||
|
@ -424,8 +423,8 @@
|
|||
<string name="dialog_mute_warning">নিঃশব্দ @%s\?</string>
|
||||
<string name="dialog_block_warning">অবরুদ্ধ @%s\?</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d দিন</item>
|
||||
<item quantity="other">%d দিন</item>
|
||||
<item quantity="one">%d দিন বাকি</item>
|
||||
<item quantity="other">%d দিন বাকি</item>
|
||||
</plurals>
|
||||
<string name="pref_title_gradient_for_media">লুকানো মিডিয়ার জন্য রঙিন গ্রেডিয়েন্ট ব্যবহার করি</string>
|
||||
<string name="action_unmute_conversation">আলাপ বন্ধ করো</string>
|
||||
|
@ -437,16 +436,16 @@
|
|||
<string name="notification_follow_request_name">অনুরোধ অনুসরণ করুন</string>
|
||||
<string name="pref_title_confirm_reblogs">বুস্ট করার আগে নিশ্চিত করো</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d সেকেন্ড</item>
|
||||
<item quantity="other">%d সেকেন্ড</item>
|
||||
<item quantity="one">%d সেকেন্ড বাকি</item>
|
||||
<item quantity="other">%d সেকেন্ড বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d মিনিট</item>
|
||||
<item quantity="other">%d মিনিট</item>
|
||||
<item quantity="one">%d মিনিট বাকি</item>
|
||||
<item quantity="other">%d মিনিট বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ঘন্টা</item>
|
||||
<item quantity="other">%d ঘন্টা</item>
|
||||
<item quantity="one">%d ঘন্টা বাকি</item>
|
||||
<item quantity="other">%d ঘন্টা বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s জন</item>
|
||||
|
|
|
@ -364,27 +364,10 @@
|
|||
<item quantity="one">%s vots</item>
|
||||
<item quantity="other">%s vots</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restants</string>
|
||||
<string name="poll_info_time_absolute">Acaba a %s</string>
|
||||
<string name="poll_info_closed">tancat</string>
|
||||
<string name="poll_ended_voted">L\'enquesta on has votat està tancada</string>
|
||||
<string name="poll_ended_created">La enquesta que heu creat ha finalitzat</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="description_status_cw">Advertència: %s</string>
|
||||
<string name="title_statuses_pinned">Toot fixat</string>
|
||||
<string name="unpin_action">Toot no fixat</string>
|
||||
|
|
|
@ -332,7 +332,7 @@
|
|||
<plurals name="favs">
|
||||
<item quantity="one"><b>%1$s</b> oblíbení</item>
|
||||
<item quantity="few"><b>%1$s</b> oblíbení</item>
|
||||
<item quantity="other"/>
|
||||
<item quantity="other"><b>%1$s</b> oblíbení</item>
|
||||
</plurals>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><b>%s</b> boost</item>
|
||||
|
@ -377,7 +377,6 @@
|
|||
<item quantity="few">%s hlasy</item>
|
||||
<item quantity="other">%s hlasů</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">zbývá %s</string>
|
||||
<string name="poll_info_time_absolute">končí v %s</string>
|
||||
<string name="poll_info_closed">uzavřena</string>
|
||||
<string name="poll_vote">Hlasovat</string>
|
||||
|
@ -388,24 +387,19 @@
|
|||
<string name="poll_ended_voted">Anketa, ve které jste hlasoval/a, skončila</string>
|
||||
<string name="poll_ended_created">Anketa, kterou jste vytvořil/a, skončila</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d den</item>
|
||||
<item quantity="few">%d dny</item>
|
||||
<item quantity="other">%d dní</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="one">zbývá %d den</item>
|
||||
<item quantity="few">zbývá %d dny</item>
|
||||
<item quantity="other">zbývá %d dní</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="few">%d minuty</item>
|
||||
<item quantity="other">%d minut</item>
|
||||
<item quantity="one">zbývá %d minuta</item>
|
||||
<item quantity="few">zbývá %d minuty</item>
|
||||
<item quantity="other">zbývá %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="few">%d sekundy</item>
|
||||
<item quantity="other">%d sekund</item>
|
||||
<item quantity="one">zbývá %d sekunda</item>
|
||||
<item quantity="few">zbývá %d sekundy</item>
|
||||
<item quantity="other">zbývá %d sekund</item>
|
||||
</plurals>
|
||||
<string name="pref_title_animate_gif_avatars">Animovat avatary GIF</string>
|
||||
<string name="description_poll">Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
|
|
|
@ -354,27 +354,26 @@
|
|||
<item quantity="one">%s Stimme</item>
|
||||
<item quantity="other">%s Stimmen</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s verbleibend</string>
|
||||
<string name="poll_info_time_absolute">endet um %s</string>
|
||||
<string name="poll_info_closed">Geschlossen</string>
|
||||
<string name="poll_vote">Abstimmen</string>
|
||||
<string name="poll_ended_voted">Eine Umfrage in der du abgestimmt hast ist vorbei</string>
|
||||
<string name="poll_ended_created">Eine Umfrage die du erstellt hast ist vorbei</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d Tag</item>
|
||||
<item quantity="other">%d Tage</item>
|
||||
<item quantity="one">%d Tag verbleibend</item>
|
||||
<item quantity="other">%d Tage verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d Stunde</item>
|
||||
<item quantity="other">%d Stunden</item>
|
||||
<item quantity="one">%d Stunde verbleibend</item>
|
||||
<item quantity="other">%d Stunden verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d Minute</item>
|
||||
<item quantity="other">%d Minuten</item>
|
||||
<item quantity="one">%d Minute verbleibend</item>
|
||||
<item quantity="other">%d Minuten verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d Sekunde</item>
|
||||
<item quantity="other">%d Sekunden</item>
|
||||
<item quantity="one">%d Sekunde verbleibend</item>
|
||||
<item quantity="other">%d Sekunden verbleibend</item>
|
||||
</plurals>
|
||||
<string name="title_domain_mutes">Versteckte Domains</string>
|
||||
<string name="action_view_domain_mutes">Versteckte Domains</string>
|
||||
|
|
|
@ -376,28 +376,12 @@
|
|||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restas</string>
|
||||
<string name="poll_info_time_absolute">finiĝos je %s</string>
|
||||
<string name="poll_info_closed">finiĝita</string>
|
||||
<string name="poll_vote">Voĉdoni</string>
|
||||
<string name="poll_ended_voted">Enketo al kiu vi voĉdonis finiĝis</string>
|
||||
<string name="poll_ended_created">Enketo kiu vi kreis finiĝis</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
|
||||
<string name="title_domain_mutes">Kaŝitaj domajnoj</string>
|
||||
<string name="action_view_domain_mutes">Kaŝitaj domajnoj</string>
|
||||
<string name="action_mute_domain">Silentigi %s</string>
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<string name="action_report">Reportar</string>
|
||||
<string name="action_delete">Borrar</string>
|
||||
<string name="action_send">Enviar</string>
|
||||
<string name="action_send_public">Publicar</string>
|
||||
<string name="action_send_public">¡Publicar!</string>
|
||||
<string name="action_retry">Reintentar</string>
|
||||
<string name="action_close">Cerrar</string>
|
||||
<string name="action_view_profile">Perfil</string>
|
||||
|
@ -330,20 +330,20 @@
|
|||
<string name="pref_title_bot_overlay">Mostrar indicador de bots</string>
|
||||
<string name="poll_vote">Votar</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d día</item>
|
||||
<item quantity="other">%d días</item>
|
||||
<item quantity="one">%d día restante</item>
|
||||
<item quantity="other">%d días restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d hora</item>
|
||||
<item quantity="other">%d horas</item>
|
||||
<item quantity="one">%d hora restante</item>
|
||||
<item quantity="other">%d horas restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuto</item>
|
||||
<item quantity="other">%d minutos</item>
|
||||
<item quantity="one">%d minuto restante</item>
|
||||
<item quantity="other">%d minutos restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d segundo</item>
|
||||
<item quantity="other">%d segundos</item>
|
||||
<item quantity="one">%d segundo restante</item>
|
||||
<item quantity="other">%d segundos restante</item>
|
||||
</plurals>
|
||||
<string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string>
|
||||
<plurals name="poll_info_votes">
|
||||
|
@ -388,7 +388,6 @@
|
|||
<string name="compose_shortcut_short_label">Redactar</string>
|
||||
<string name="notification_clear_text">¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\?</string>
|
||||
<string name="compose_preview_image_description">Acciones para la imagen %s</string>
|
||||
<string name="poll_info_time_relative">%s restante</string>
|
||||
<string name="poll_info_time_absolute">termina en %s</string>
|
||||
<string name="poll_ended_voted">Una encuesta en la que has votado ha terminado</string>
|
||||
<string name="poll_ended_created">Una encuesta que has creado ha terminado</string>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<string name="footer_empty">Edukirik ez. Arrastatu behera birkargatzeko!</string>
|
||||
<string name="notification_reblog_format">%s-(e)k zure tuta bultzatu du</string>
|
||||
<string name="notification_favourite_format">%s-(e)k zure tuta gogoko du</string>
|
||||
<string name="notification_follow_format">%s-(e)k jarraitu dizu</string>
|
||||
<string name="notification_follow_format">%s(e)k jarraitu zaitu</string>
|
||||
<string name="report_username_format">\@%s salatu</string>
|
||||
<string name="report_comment_hint">Informazio gehigarria?</string>
|
||||
<string name="action_quick_reply">Erantzun azkarra</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="action_view_favourites">Gogokoak</string>
|
||||
<string name="action_view_mutes">Isilduak</string>
|
||||
<string name="action_view_blocks">Blokeatuak</string>
|
||||
<string name="action_view_follow_requests">Eskariak</string>
|
||||
<string name="action_view_follow_requests">Eskakizunak</string>
|
||||
<string name="action_view_media">Multimedia</string>
|
||||
<string name="action_open_in_web">Nabigatzailean ireki</string>
|
||||
<string name="action_add_media">Multimedia erantsi</string>
|
||||
|
@ -129,7 +129,7 @@
|
|||
<string name="dialog_title_finishing_media_upload">Mediaren igoera bukatzen</string>
|
||||
<string name="dialog_message_uploading_media">Igotzen…</string>
|
||||
<string name="dialog_download_image">Jaitsi</string>
|
||||
<string name="dialog_message_cancel_follow_request">Jarraipen-eskaerari uko egin\?</string>
|
||||
<string name="dialog_message_cancel_follow_request">Jarraipen-eskakizunari uko egin\?</string>
|
||||
<string name="dialog_unfollow_warning">Kontu hau jarraitzeari utzi\?</string>
|
||||
<string name="dialog_delete_toot_warning">Tuta ezabatu\?</string>
|
||||
<string name="visibility_public">Publikoa: Istorio publikoetan erakutsi</string>
|
||||
|
@ -156,7 +156,7 @@
|
|||
<string name="app_theme_auto">Automatikoa</string>
|
||||
<string name="pref_title_browser_settings">Nabigatzailea</string>
|
||||
<string name="pref_title_custom_tabs">Chromeko fitxak erabili</string>
|
||||
<string name="pref_title_hide_follow_button">Tut egiteko botoia ezkutatu beherantz joaterakoan.</string>
|
||||
<string name="pref_title_hide_follow_button">Tut egiteko botoia ezkutatu beherantz joaterakoan</string>
|
||||
<string name="pref_title_status_filter">Denbora-lerro filtroak</string>
|
||||
<string name="pref_title_status_tabs">Fitxak</string>
|
||||
<string name="pref_title_show_boosts">Bultzadak erakutsi</string>
|
||||
|
@ -279,7 +279,7 @@
|
|||
<string name="pin_action">Ainguratu</string>
|
||||
<string name="error_network">Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez!</string>
|
||||
<string name="title_direct_messages">Mezu Zuzenak</string>
|
||||
<string name="title_tab_preferences">Kategoriak</string>
|
||||
<string name="title_tab_preferences">Fitxak</string>
|
||||
<string name="title_statuses_pinned">Lotuta</string>
|
||||
<string name="title_domain_mutes">Ezkutuko domeinuak</string>
|
||||
<string name="title_scheduled_toot">Programatutako tutak</string>
|
||||
|
@ -291,7 +291,7 @@
|
|||
<string name="action_delete_and_redraft">Ezabatu eta zirriborroa berriro egin</string>
|
||||
<string name="action_view_domain_mutes">Ezkutuko domeinuak</string>
|
||||
<string name="action_add_poll">Galdeketa gehitu</string>
|
||||
<string name="action_mute_domain">%s isilarazi</string>
|
||||
<string name="action_mute_domain">Mututu %s</string>
|
||||
<string name="action_access_scheduled_toot">Programatutako tutak</string>
|
||||
<string name="action_schedule_toot">Tuta programatu</string>
|
||||
<string name="action_reset_schedule">Berrezarri</string>
|
||||
|
@ -361,7 +361,7 @@
|
|||
<string name="conversation_1_recipients">%1$s</string>
|
||||
<string name="conversation_2_recipients">%1$s eta %2$s</string>
|
||||
<string name="conversation_more_recipients">%1$s, %2$s eta %3$d gehiago</string>
|
||||
<string name="max_tab_number_reached">geienezko %1$d fitxa iritsita</string>
|
||||
<string name="max_tab_number_reached">gehienezko %1$d fitxa iritsita</string>
|
||||
<string name="description_status_media">Media: %s</string>
|
||||
<string name="description_status_cw">Edukiaren abisua: %s</string>
|
||||
<string name="description_status_media_no_description_placeholder">Deskribapenik ez</string>
|
||||
|
@ -392,20 +392,16 @@
|
|||
<string name="poll_ended_voted">Botoa eman duzun galdeketa amaitu da</string>
|
||||
<string name="poll_ended_created">Sortu duzun galdeketa amaitu da</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">Egun %d</item>
|
||||
<item quantity="other">%d egun</item>
|
||||
<item quantity="one">Egun %d geratzen da</item>
|
||||
<item quantity="other">%d egun geratzen da</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">Ordu %d</item>
|
||||
<item quantity="other">%d ordu</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">Ordu %d geratzen da</item>
|
||||
<item quantity="other">%d ordu geratzen da</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">Segundu %d</item>
|
||||
<item quantity="other">%d segundu</item>
|
||||
<item quantity="one">Segundu %d geratzen da</item>
|
||||
<item quantity="other">%d segundu geratzen da</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Jarraitu</string>
|
||||
<string name="button_back">Itzuli</string>
|
||||
|
@ -439,7 +435,6 @@
|
|||
<string name="action_open_reblogger">Ireki bultzadaren egilea</string>
|
||||
<string name="pref_title_public_filter_keywords">Denbora lerro publikoak</string>
|
||||
<string name="description_status_bookmarked">Laster-markatuta</string>
|
||||
<string name="poll_info_time_relative">%s geratzen da</string>
|
||||
<string name="error_audio_upload_size">Audioak 40MB baino gutxiago izan behar ditu.</string>
|
||||
<string name="select_list_title">Aukeratu zerrenda</string>
|
||||
<string name="list">Zerrenda</string>
|
||||
|
@ -450,13 +445,31 @@
|
|||
<string name="notification_follow_request_description">Jarraitzeko eskaereri buruzko jakinarazpenak</string>
|
||||
<string name="dialog_mute_warning">\@%s isildu\?</string>
|
||||
<string name="dialog_block_warning">\@%s blokeatu\?</string>
|
||||
<string name="action_mute_conversation">Elkarrizketa isildu</string>
|
||||
<string name="notification_follow_request_format">%s -k zu jarraitzeko eskatu dizu</string>
|
||||
<string name="action_mute_conversation">Mututu elkarrizketa</string>
|
||||
<string name="notification_follow_request_format">%s(e)k zu jarraitzeko eskatu dizu</string>
|
||||
<string name="hashtags">Traolak</string>
|
||||
<string name="dialog_mute_hide_notifications">Ez erakutsi jakinarazpenak</string>
|
||||
<string name="action_unmute_desc">Desmututu %s</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">Pertsona %1</item>
|
||||
<item quantity="other">%2 pertsona</item>
|
||||
</plurals>
|
||||
<string name="pref_title_hide_top_toolbar">Ezkutatu goiko tresna-barraren izenburua</string>
|
||||
<string name="pref_title_confirm_reblogs">Erakutsi berrespen-abisua tuta bultzatu aurretik</string>
|
||||
<string name="pref_title_show_cards_in_timelines">Erakutsi esteken aurrebista denbora-lerroetan</string>
|
||||
<string name="pref_title_enable_swipe_for_tabs">Gaitu pasatze-keinua fitxetan zehar aldatzeko</string>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">Minutu %d faltan</item>
|
||||
<item quantity="other">%d minutu faltan</item>
|
||||
</plurals>
|
||||
<string name="add_hashtag_title">Gehitu traola</string>
|
||||
<string name="pref_main_nav_position_option_bottom">Azpia</string>
|
||||
<string name="pref_main_nav_position_option_top">Goia</string>
|
||||
<string name="pref_main_nav_position">Nabigatze posizio nagusia</string>
|
||||
<string name="pref_title_gradient_for_media">Erakutsi gradiente koloretsua ezkutuko mediarentzako</string>
|
||||
<string name="pref_title_notification_filter_follow_requests">jarraipena-eskaera</string>
|
||||
<string name="action_unmute_conversation">Desmututu elkarrizketa</string>
|
||||
<string name="action_unmute_domain">Desmututu %s</string>
|
||||
<string name="action_mute_notifications_desc">Mututu %s(r)en jakinarazpenak</string>
|
||||
<string name="action_unmute_notifications_desc">Desmututu %s(r)en jakinarazpenak</string>
|
||||
</resources>
|
|
@ -371,23 +371,22 @@
|
|||
<string name="compose_shortcut_long_label">ایجاد بوق</string>
|
||||
<string name="compose_shortcut_short_label">ایجاد</string>
|
||||
<string name="notification_clear_text">مطمئنید میخواهید تمام آگاهیهایتان را برای همیشه پاک کنید؟</string>
|
||||
<string name="poll_info_time_relative">%s باقی مانده</string>
|
||||
<string name="poll_info_time_absolute">پایان در %s</string>
|
||||
<string name="poll_info_closed">بسته</string>
|
||||
<string name="poll_vote">رأی</string>
|
||||
<string name="poll_ended_voted">یک نظرسنجی که در آن رأی دادید، تمام شد</string>
|
||||
<string name="poll_ended_created">یک نظرسنجی که ساختید، تمام شد</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d روز</item>
|
||||
<item quantity="other">%d روز</item>
|
||||
<item quantity="one">%d روز مانده</item>
|
||||
<item quantity="other">%d روز مانده</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ساعت</item>
|
||||
<item quantity="other">%d ساعت</item>
|
||||
<item quantity="one">%d ساعت مانده</item>
|
||||
<item quantity="other">%d ساعت مانده</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d دقیقه</item>
|
||||
<item quantity="other">%d دقیقه</item>
|
||||
<item quantity="one">%d دقیقه مانده</item>
|
||||
<item quantity="other">%d دقیقه مانده</item>
|
||||
</plurals>
|
||||
<string name="button_continue">ادامه</string>
|
||||
<string name="button_back">بازگشت</string>
|
||||
|
@ -453,8 +452,8 @@
|
|||
<string name="action_mute_conversation">خموشی گفتوگو</string>
|
||||
<string name="notification_follow_request_format">%s میخواهد پیگیرتان شود</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d ثانیه</item>
|
||||
<item quantity="other">%d ثانیه</item>
|
||||
<item quantity="one">%d ثانیه مانده</item>
|
||||
<item quantity="other">%d ثانیه مانده</item>
|
||||
</plurals>
|
||||
<string name="pref_main_nav_position_option_bottom">پایین</string>
|
||||
<string name="pref_main_nav_position_option_top">بالا</string>
|
||||
|
|
|
@ -372,7 +372,6 @@
|
|||
<string name="notification_clear_text">Désirez-vous nettoyer toutes vos notifications de façon permanente \?</string>
|
||||
<string name="action_delete_and_redraft">Effacer et ré-écrire</string>
|
||||
<string name="dialog_redraft_toot_warning">Effacer et ré-écrire ce pouet \?</string>
|
||||
<string name="poll_info_time_relative">%s restant</string>
|
||||
<string name="poll_info_time_absolute">Termina à %s</string>
|
||||
<string name="poll_info_closed">Terminé</string>
|
||||
<string name="poll_vote">Voter</string>
|
||||
|
@ -381,20 +380,20 @@
|
|||
<string name="notification_poll_description">Notifications pour les sondages terminés</string>
|
||||
<string name="poll_ended_created">Un sondage que vous avez créé est terminé</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d jour</item>
|
||||
<item quantity="other">%d jours</item>
|
||||
<item quantity="one">%d jour restant</item>
|
||||
<item quantity="other">%d jours restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d heure</item>
|
||||
<item quantity="other">%d heures</item>
|
||||
<item quantity="one">%d heure restant</item>
|
||||
<item quantity="other">%d heures restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minute</item>
|
||||
<item quantity="other">%d minutes</item>
|
||||
<item quantity="one">%d minute restant</item>
|
||||
<item quantity="other">%d minutes restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d seconde</item>
|
||||
<item quantity="other">%d secondes</item>
|
||||
<item quantity="one">%d seconde restant</item>
|
||||
<item quantity="other">%d secondes restant</item>
|
||||
</plurals>
|
||||
<string name="pref_title_animate_gif_avatars">Activer l’animation des avatars</string>
|
||||
<string name="compose_preview_image_description">Actions pour l’image %s</string>
|
||||
|
|
|
@ -297,11 +297,11 @@
|
|||
<string name="notification_clear_text">An bhfuil tú cinnte gur mhaith leat do chuid fógraí go léir a ghlanadh go buan\?</string>
|
||||
<string name="poll_ended_created">Tá deireadh le vótaíocht a chruthaigh tú</string>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">$d nóiméad</item>
|
||||
<item quantity="two">$d nóiméad</item>
|
||||
<item quantity="few">$d nóiméad</item>
|
||||
<item quantity="many">$d nóiméad</item>
|
||||
<item quantity="other">$d nóiméad</item>
|
||||
<item quantity="one">D\'imigh $d nóiméad</item>
|
||||
<item quantity="two">D\'imigh $d nóiméad</item>
|
||||
<item quantity="few">D\'imigh $d nóiméad</item>
|
||||
<item quantity="many">D\'imigh $d nóiméad</item>
|
||||
<item quantity="other">D\'imigh $d nóiméad</item>
|
||||
</plurals>
|
||||
<string name="failed_fetch_statuses">Theip ar stádas a fháil</string>
|
||||
<string name="report_description_1">Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos:</string>
|
||||
|
@ -407,7 +407,6 @@
|
|||
<item quantity="many">%s daoine</item>
|
||||
<item quantity="other">%s daoine</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">D\'imigh %s</string>
|
||||
<string name="poll_info_time_absolute">foircinn ag %s</string>
|
||||
<string name="poll_info_closed">dúnta</string>
|
||||
<string name="poll_vote">Vóta</string>
|
||||
|
@ -420,18 +419,18 @@
|
|||
<item quantity="other">%d lá</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d uair</item>
|
||||
<item quantity="two">%d uair an chloig</item>
|
||||
<item quantity="few">%d uair an chloig</item>
|
||||
<item quantity="many">%d uair an chloig</item>
|
||||
<item quantity="other">%d uair an chloig</item>
|
||||
<item quantity="one">D\'imigh %d uair</item>
|
||||
<item quantity="two">D\'imigh %d uair an chloig</item>
|
||||
<item quantity="few">D\'imigh %d uair an chloig</item>
|
||||
<item quantity="many">D\'imigh %d uair an chloig</item>
|
||||
<item quantity="other">D\'imigh %d uair an chloig</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d soicind</item>
|
||||
<item quantity="two">%d soicind</item>
|
||||
<item quantity="few">%d soicind</item>
|
||||
<item quantity="many">%d soicind</item>
|
||||
<item quantity="other">%d soicind</item>
|
||||
<item quantity="one">D\'imigh %d soicind</item>
|
||||
<item quantity="two">D\'imigh %d soicind</item>
|
||||
<item quantity="few">D\'imigh %d soicind</item>
|
||||
<item quantity="many">D\'imigh %d soicind</item>
|
||||
<item quantity="other">D\'imigh %d soicind</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Lean ar aghaidh</string>
|
||||
<string name="button_back">Ar ais</string>
|
||||
|
|
|
@ -208,17 +208,16 @@
|
|||
<string name="poll_duration_30_min">30 मिनिट</string>
|
||||
<string name="poll_duration_5_min">5 मिनट</string>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d घंटा</item>
|
||||
<item quantity="other">%d घंटे</item>
|
||||
<item quantity="one">%d घंटा शेष</item>
|
||||
<item quantity="other">%d घंटे शेष</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d दिन</item>
|
||||
<item quantity="other">%d दिन</item>
|
||||
<item quantity="one">%d दिन शेष</item>
|
||||
<item quantity="other">%d दिन शेष</item>
|
||||
</plurals>
|
||||
<string name="poll_ended_created">आपके द्वारा बनाया गया एक जनमत समाप्त हो गया है</string>
|
||||
<string name="poll_ended_voted">आपके द्वारा मतदान किया गया जनमत समाप्त हो गया है</string>
|
||||
<string name="poll_info_time_absolute">%s समाप्त होगा</string>
|
||||
<string name="poll_info_time_relative">%s शेष</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s व्यक्ति</item>
|
||||
<item quantity="other">%s लोग</item>
|
||||
|
@ -400,12 +399,8 @@
|
|||
<item quantity="one"><b>%1$s</b> ने पसंद किया</item>
|
||||
<item quantity="other"><b>%1$s</b> ने पसंद किया</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d सेकेंड</item>
|
||||
<item quantity="other">%d सेकेंड</item>
|
||||
<item quantity="one">%d सेकेंड शेष</item>
|
||||
<item quantity="other">%d सेकेंड शेष</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -380,27 +380,26 @@
|
|||
<item quantity="one">%s szavazat</item>
|
||||
<item quantity="other">%s szavazat</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s maradt</string>
|
||||
<string name="poll_info_time_absolute">vége %s</string>
|
||||
<string name="poll_info_closed">véget ért</string>
|
||||
<string name="poll_vote">Szavazás</string>
|
||||
<string name="poll_ended_voted">Egy szavazás véget ért, melyben részt vettél</string>
|
||||
<string name="poll_ended_created">Egy szavazás véget ért, melyet te hoztál létre</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d nap</item>
|
||||
<item quantity="other">%d nap</item>
|
||||
<item quantity="one">%d nap maradt</item>
|
||||
<item quantity="other">%d nap maradt</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d óra</item>
|
||||
<item quantity="other">%d óra</item>
|
||||
<item quantity="one">%d óra maradt</item>
|
||||
<item quantity="other">%d óra maradt</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d perc</item>
|
||||
<item quantity="other">%d perc</item>
|
||||
<item quantity="one">%d perc maradt</item>
|
||||
<item quantity="other">%d perc maradt</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d másodperc</item>
|
||||
<item quantity="other">%d másodperc</item>
|
||||
<item quantity="one">%d másodperc maradt</item>
|
||||
<item quantity="other">%d másodperc maradt</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Folytatás</string>
|
||||
<string name="button_back">Vissza</string>
|
||||
|
|
|
@ -377,7 +377,6 @@
|
|||
<item quantity="one">%s atkvæði</item>
|
||||
<item quantity="other">%s atkvæði</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s eftir</string>
|
||||
<string name="poll_info_time_absolute">lýkur %s</string>
|
||||
<string name="poll_info_closed">lokað</string>
|
||||
<string name="poll_vote">Greiða atkvæði</string>
|
||||
|
@ -424,19 +423,19 @@
|
|||
<item quantity="other"><b>%s</b> Endurbirtingar</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekúnda</item>
|
||||
<item quantity="other">%d sekúndur</item>
|
||||
<item quantity="one">%d sekúnda eftir</item>
|
||||
<item quantity="other">%d sekúndur eftir</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d mínúta</item>
|
||||
<item quantity="other">%d mínútur</item>
|
||||
<item quantity="one">%d mínúta eftir</item>
|
||||
<item quantity="other">%d mínútur eftir</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d klukkustund</item>
|
||||
<item quantity="other">%d klukkustundir</item>
|
||||
<item quantity="one">%d klukkustund eftir</item>
|
||||
<item quantity="other">%d klukkustundir eftir</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dagur</item>
|
||||
<item quantity="other">%d dagar</item>
|
||||
<item quantity="one">%d dagur eftir</item>
|
||||
<item quantity="other">%d dagar eftir</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -371,7 +371,6 @@
|
|||
<item quantity="one">%s voto</item>
|
||||
<item quantity="other">%s voti</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s rimasti</string>
|
||||
<string name="poll_info_time_absolute">termina alle %s</string>
|
||||
<string name="poll_info_closed">terminato</string>
|
||||
<string name="poll_vote">Vota</string>
|
||||
|
@ -403,16 +402,16 @@
|
|||
<string name="poll_ended_voted">Un sondaggio che hai votato è terminato</string>
|
||||
<string name="poll_ended_created">Un sondaggio che hai creato è terminato</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d giorno</item>
|
||||
<item quantity="other">%d giorni</item>
|
||||
<item quantity="one">%d giorno rimasti</item>
|
||||
<item quantity="other">%d giorni rimasti</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ora</item>
|
||||
<item quantity="other">%d ore</item>
|
||||
<item quantity="one">%d ora rimasti</item>
|
||||
<item quantity="other">%d ore rimasti</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuto</item>
|
||||
<item quantity="other">%d minuti</item>
|
||||
<item quantity="one">%d minuto rimasti</item>
|
||||
<item quantity="other">%d minuti rimasti</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d secondo</item>
|
||||
|
|
|
@ -343,19 +343,18 @@
|
|||
<plurals name="poll_info_votes">
|
||||
<item quantity="other">%s票</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">残り%s</string>
|
||||
<string name="poll_vote">投票</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="other">%d日</item>
|
||||
<item quantity="other">残り%d日</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="other">%d時間</item>
|
||||
<item quantity="other">残り%d時間</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="other">%d分</item>
|
||||
<item quantity="other">残り%d分</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="other">%d秒</item>
|
||||
<item quantity="other">残り%d秒</item>
|
||||
</plurals>
|
||||
<string name="title_domain_mutes">非表示のドメイン</string>
|
||||
<string name="action_view_domain_mutes">非表示のドメイン</string>
|
||||
|
|
|
@ -174,23 +174,22 @@
|
|||
<item quantity="one">%s n wedɣar</item>
|
||||
<item quantity="other">%s n yedɣaren</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s id yugran</string>
|
||||
<string name="poll_info_time_absolute">ad ifak deg %s</string>
|
||||
<string name="poll_info_closed">ifuk</string>
|
||||
<string name="poll_vote">Dɣer</string>
|
||||
<string name="poll_ended_voted">Ifuk, tura kan, yiwen wedɣar t tteki-iḍ degs</string>
|
||||
<string name="poll_ended_created">Ifukk yiwen wedɣar id snulfaḍ</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d n wass</item>
|
||||
<item quantity="other">%d n wussan</item>
|
||||
<item quantity="one">%d n wass id yugran</item>
|
||||
<item quantity="other">%d n wussan id yugran</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d wesrag</item>
|
||||
<item quantity="other">%d n yisragen</item>
|
||||
<item quantity="one">%d wesrag id yugran</item>
|
||||
<item quantity="other">%d n yisragen id yugran</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d n tasdidt</item>
|
||||
<item quantity="other">%d n tisdidin</item>
|
||||
<item quantity="one">%d n tasdidt id yugran</item>
|
||||
<item quantity="other">%d n tisdidin id yugran</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Kemmel</string>
|
||||
<string name="button_back">Uɣal</string>
|
||||
|
@ -232,8 +231,8 @@
|
|||
<string name="abbreviated_in_minutes">deg %dtsd</string>
|
||||
<string name="abbreviated_in_seconds">deg %dtsn</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d n tasint</item>
|
||||
<item quantity="other">%d n tasinin</item>
|
||||
<item quantity="one">%d n tasint id yugran</item>
|
||||
<item quantity="other">%d n tasinin id yugran</item>
|
||||
</plurals>
|
||||
<string name="status_sensitive_media_title">Agbur amḥulfu</string>
|
||||
<string name="pref_default_media_sensitivity">Creḍ allal n teywalt amzun d amḥulfu</string>
|
||||
|
|
|
@ -374,7 +374,6 @@
|
|||
<plurals name="poll_info_votes">
|
||||
<item quantity="other">%s 명 참여</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s 남음</string>
|
||||
<string name="poll_info_time_absolute">%s에 종료</string>
|
||||
<string name="poll_info_closed">마감됨</string>
|
||||
<string name="poll_vote">투표</string>
|
||||
|
@ -382,16 +381,16 @@
|
|||
<string name="poll_ended_created">당신이 시작한 투표가 종료되었습니다</string>
|
||||
<!--These are for timestamps on polls -->
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="other">%d일</item>
|
||||
<item quantity="other">%d일 남음</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="other">%d시간</item>
|
||||
<item quantity="other">%d시간 남음</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="other">%d분</item>
|
||||
<item quantity="other">%d분 남음</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="other">%d초</item>
|
||||
<item quantity="other">%d초 남음</item>
|
||||
</plurals>
|
||||
<string name="button_continue">다음</string>
|
||||
<string name="button_back">이전</string>
|
||||
|
|
|
@ -371,7 +371,6 @@
|
|||
<item quantity="one">%s stem</item>
|
||||
<item quantity="other">%s stemmen</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s over</string>
|
||||
<string name="poll_info_time_absolute">eindigt op %s</string>
|
||||
<string name="poll_info_closed">gesloten</string>
|
||||
<string name="poll_vote">Stemmen</string>
|
||||
|
@ -391,20 +390,20 @@
|
|||
<string name="description_poll">Poll met keuzes: %s, %s, %s, %s; %s</string>
|
||||
<string name="compose_preview_image_description">Acties voor afbeelding %s</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dag</item>
|
||||
<item quantity="other">%d dagen</item>
|
||||
<item quantity="one">%d dag over</item>
|
||||
<item quantity="other">%d dagen over</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d uur</item>
|
||||
<item quantity="other">%d uur</item>
|
||||
<item quantity="one">%d uur over</item>
|
||||
<item quantity="other">%d uur over</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuut</item>
|
||||
<item quantity="other">%d minuten</item>
|
||||
<item quantity="one">%d minuut over</item>
|
||||
<item quantity="other">%d minuten over</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d seconde</item>
|
||||
<item quantity="other">%d seconden</item>
|
||||
<item quantity="one">%d seconde over</item>
|
||||
<item quantity="other">%d seconden over</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Doorgaan</string>
|
||||
<string name="button_back">Terug</string>
|
||||
|
|
|
@ -343,7 +343,6 @@
|
|||
<item quantity="one">%s stemme</item>
|
||||
<item quantity="other">%s stemmer</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s igjen</string>
|
||||
<string name="poll_info_time_absolute">avsluttes %s</string>
|
||||
<string name="poll_info_closed">stengt</string>
|
||||
<string name="poll_vote">Stem</string>
|
||||
|
@ -366,20 +365,20 @@
|
|||
<string name="poll_ended_voted">En avstemming du har stemt på er avsluttet</string>
|
||||
<string name="poll_ended_created">En avstemming du opprettet er avsluttet</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dag</item>
|
||||
<item quantity="other">%d dager</item>
|
||||
<item quantity="one">%d dag igjen</item>
|
||||
<item quantity="other">%d dager igjen</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d time</item>
|
||||
<item quantity="other">%d timer</item>
|
||||
<item quantity="one">%d time igjen</item>
|
||||
<item quantity="other">%d timer igjen</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minutt</item>
|
||||
<item quantity="other">%d minutter</item>
|
||||
<item quantity="one">%d minutt igjen</item>
|
||||
<item quantity="other">%d minutter igjen</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekund</item>
|
||||
<item quantity="other">%d sekunder</item>
|
||||
<item quantity="one">%d sekund igjen</item>
|
||||
<item quantity="other">%d sekunder igjen</item>
|
||||
</plurals>
|
||||
<string name="compose_preview_image_description">Handlinger for bilde %s</string>
|
||||
<string name="pref_title_animate_gif_avatars">Animer GIF-avatarer</string>
|
||||
|
@ -464,4 +463,6 @@
|
|||
<string name="action_unmute_notifications_desc">Fjern demping av varsler fra %s</string>
|
||||
<string name="action_unmute_desc">Fjern demping av %s</string>
|
||||
<string name="pref_title_hide_top_toolbar">Skjul tittelen på den øverste verktøylinjen</string>
|
||||
<string name="account_note_saved">Lagret!</string>
|
||||
<string name="account_note_hint">Ditt private notat om denne kontoen</string>
|
||||
</resources>
|
|
@ -366,7 +366,6 @@
|
|||
<item quantity="one">%s vòte</item>
|
||||
<item quantity="other">%s votes</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restant</string>
|
||||
<string name="poll_info_time_absolute">S’acaba a %s</string>
|
||||
<string name="poll_info_closed">acabat</string>
|
||||
<string name="poll_vote">Votar</string>
|
||||
|
@ -377,20 +376,20 @@
|
|||
<string name="poll_ended_voted">Un sondatge ont avètz votat es acabat</string>
|
||||
<string name="poll_ended_created">Un sondatge qu’avètz creat es acabat</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d jorn</item>
|
||||
<item quantity="other">%d jorns</item>
|
||||
<item quantity="one">%d jorn restant</item>
|
||||
<item quantity="other">%d jorns restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ora</item>
|
||||
<item quantity="other">%d oras</item>
|
||||
<item quantity="one">%d ora restant</item>
|
||||
<item quantity="other">%d oras restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="other">%d minutas</item>
|
||||
<item quantity="one">%d minuta restant</item>
|
||||
<item quantity="other">%d minutas restant</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d segonda</item>
|
||||
<item quantity="other">%d segondas</item>
|
||||
<item quantity="one">%d segonda restant</item>
|
||||
<item quantity="other">%d segondas restant</item>
|
||||
</plurals>
|
||||
<string name="compose_preview_image_description">Accions per l’imatge %s</string>
|
||||
<string name="pref_title_animate_gif_avatars">Activar l’animacion dels avatars</string>
|
||||
|
|
|
@ -387,35 +387,34 @@
|
|||
<item quantity="many">%s głosów</item>
|
||||
<item quantity="other">%s głosów</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">Zostało %s</string>
|
||||
<string name="poll_info_time_absolute">kończy się %s</string>
|
||||
<string name="poll_info_closed">zakończone</string>
|
||||
<string name="poll_vote">Głosuj</string>
|
||||
<string name="poll_ended_voted">Głosowanie w którym brałeś(-aś) udział zakończyła się</string>
|
||||
<string name="poll_ended_created">Ankieta, którą stworzyłeś(aś), zakończyła się</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dzień</item>
|
||||
<item quantity="few">%d dni</item>
|
||||
<item quantity="many">%d dni</item>
|
||||
<item quantity="other">%d dni</item>
|
||||
<item quantity="one">Zostało %d dzień</item>
|
||||
<item quantity="few">Zostało %d dni</item>
|
||||
<item quantity="many">Zostało %d dni</item>
|
||||
<item quantity="other">Zostało %d dni</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d godzina</item>
|
||||
<item quantity="few">%d godziny</item>
|
||||
<item quantity="many">%d godzin</item>
|
||||
<item quantity="other">%d godzin</item>
|
||||
<item quantity="one">Zostało %d godzina</item>
|
||||
<item quantity="few">Zostało %d godziny</item>
|
||||
<item quantity="many">Zostało %d godzin</item>
|
||||
<item quantity="other">Zostało %d godzin</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="few">%d minuty</item>
|
||||
<item quantity="many">%d minut</item>
|
||||
<item quantity="other">%d minut</item>
|
||||
<item quantity="one">Zostało %d minuta</item>
|
||||
<item quantity="few">Zostało %d minuty</item>
|
||||
<item quantity="many">Zostało %d minut</item>
|
||||
<item quantity="other">Zostało %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="few">%d sekund</item>
|
||||
<item quantity="many">%d sekund</item>
|
||||
<item quantity="other">%d sekund</item>
|
||||
<item quantity="one">Zostało %d sekunda</item>
|
||||
<item quantity="few">Zostało %d sekund</item>
|
||||
<item quantity="many">Zostało %d sekund</item>
|
||||
<item quantity="other">Zostało %d sekund</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Kontynuuj</string>
|
||||
<string name="button_back">Wstecz</string>
|
||||
|
|
|
@ -369,27 +369,26 @@
|
|||
<item quantity="one">%s voto</item>
|
||||
<item quantity="other">%s votos</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restante</string>
|
||||
<string name="poll_info_time_absolute">termina em %s</string>
|
||||
<string name="poll_info_closed">Terminou</string>
|
||||
<string name="poll_vote">Votar</string>
|
||||
<string name="poll_ended_voted">Uma enquete que você votou terminou</string>
|
||||
<string name="poll_ended_created">Sua enquete terminou</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dia</item>
|
||||
<item quantity="other">%d dias</item>
|
||||
<item quantity="one">%d dia restante</item>
|
||||
<item quantity="other">%d dias restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d hora</item>
|
||||
<item quantity="other">%d horas</item>
|
||||
<item quantity="one">%d hora restante</item>
|
||||
<item quantity="other">%d horas restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuto</item>
|
||||
<item quantity="other">%d minutos</item>
|
||||
<item quantity="one">%d minuto restante</item>
|
||||
<item quantity="other">%d minutos restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d segundo</item>
|
||||
<item quantity="other">%d segundos</item>
|
||||
<item quantity="one">%d segundo restante</item>
|
||||
<item quantity="other">%d segundos restante</item>
|
||||
</plurals>
|
||||
<string name="pref_title_animate_gif_avatars">Reproduzir GIFs</string>
|
||||
<string name="description_poll">Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
|
|
|
@ -410,7 +410,6 @@
|
|||
<item quantity="many">%s голосов</item>
|
||||
<item quantity="other">%s голосов</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s</string>
|
||||
<string name="poll_info_time_absolute">завершится %s</string>
|
||||
<string name="poll_info_closed">завершён</string>
|
||||
<string name="poll_vote">Голосовать</string>
|
||||
|
|
|
@ -397,27 +397,26 @@
|
|||
<string name="button_back">पूर्वम्</string>
|
||||
<string name="button_continue">निरन्तरम्</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d क्षणः</item>
|
||||
<item quantity="other">%d क्षणौ</item>
|
||||
<item quantity="one">%d क्षणम्</item>
|
||||
<item quantity="other">%d क्षणे</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d निमेषः</item>
|
||||
<item quantity="other">%d निमेषौ</item>
|
||||
<item quantity="one">%d निमेषः शेषम्</item>
|
||||
<item quantity="other">%d निमेषौ शेषम्</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d घण्टा</item>
|
||||
<item quantity="other">%d घण्टे</item>
|
||||
<item quantity="one">%d घण्टा शेषम्</item>
|
||||
<item quantity="other">%d घण्टे शेषम्</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d दिनम्</item>
|
||||
<item quantity="other">%d दिने</item>
|
||||
<item quantity="one">%d दिनम् शेषम्</item>
|
||||
<item quantity="other">%d दिने शेषम्</item>
|
||||
</plurals>
|
||||
<string name="poll_ended_created">त्वया रचितमेकं मतदानं समाप्तम्</string>
|
||||
<string name="poll_ended_voted">मतदानमेकं समाप्तं यस्मिन् त्वयाऽपि स्वीयमतं दत्तम्</string>
|
||||
<string name="poll_vote">मतम्</string>
|
||||
<string name="poll_info_closed">पिहितम्</string>
|
||||
<string name="poll_info_time_absolute">समापनं यावत् %s</string>
|
||||
<string name="poll_info_time_relative">%s शेषम्</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s जनः</item>
|
||||
<item quantity="other">%s जनौ</item>
|
||||
|
|
|
@ -342,7 +342,6 @@
|
|||
<item quantity="few">%s glasovi</item>
|
||||
<item quantity="other">%s glasov</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">še %s</string>
|
||||
<string name="poll_info_time_absolute">se konča ob %s</string>
|
||||
<string name="poll_info_closed">zaprto</string>
|
||||
<string name="poll_vote">Glasovanje</string>
|
||||
|
@ -366,22 +365,22 @@
|
|||
<string name="poll_ended_voted">Anketa, na kateri ste glasovali, se je končala</string>
|
||||
<string name="poll_ended_created">Anketa, ki ste jo ustvarili, se je končala</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dan</item>
|
||||
<item quantity="two">%d dni</item>
|
||||
<item quantity="few">%d dni</item>
|
||||
<item quantity="other">%d dni</item>
|
||||
<item quantity="one">še %d dan</item>
|
||||
<item quantity="two">še %d dni</item>
|
||||
<item quantity="few">še %d dni</item>
|
||||
<item quantity="other">še %d dni</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ura</item>
|
||||
<item quantity="two">%d uri</item>
|
||||
<item quantity="few">%d ure</item>
|
||||
<item quantity="other">%d ur</item>
|
||||
<item quantity="one">še %d ura</item>
|
||||
<item quantity="two">še %d uri</item>
|
||||
<item quantity="few">še %d ure</item>
|
||||
<item quantity="other">še %d ur</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="two">%d minuti</item>
|
||||
<item quantity="few">%d minute</item>
|
||||
<item quantity="other">%d minut</item>
|
||||
<item quantity="one">še %d minuta</item>
|
||||
<item quantity="two">še %d minuti</item>
|
||||
<item quantity="few">še %d minute</item>
|
||||
<item quantity="other">še %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekunda</item>
|
||||
|
|
|
@ -373,7 +373,6 @@
|
|||
<item quantity="one">%s röst</item>
|
||||
<item quantity="other">%s röster</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s kvar</string>
|
||||
<string name="poll_info_time_absolute">avslutas vid %s</string>
|
||||
<string name="poll_info_closed">stängd</string>
|
||||
<string name="poll_vote">Rösta</string>
|
||||
|
@ -383,20 +382,20 @@
|
|||
<string name="poll_ended_voted">En omröstning där du har röstat är avslutad</string>
|
||||
<string name="poll_ended_created">En omröstning som du har skapat har avslutats</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d dag</item>
|
||||
<item quantity="other">%d dagar</item>
|
||||
<item quantity="one">%d dag kvar</item>
|
||||
<item quantity="other">%d dagar kvar</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d timme</item>
|
||||
<item quantity="other">%d timmar</item>
|
||||
<item quantity="one">%d timme kvar</item>
|
||||
<item quantity="other">%d timmar kvar</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minut</item>
|
||||
<item quantity="other">%d minuter</item>
|
||||
<item quantity="one">%d minut kvar</item>
|
||||
<item quantity="other">%d minuter kvar</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d sekund</item>
|
||||
<item quantity="other">%d sekunder</item>
|
||||
<item quantity="one">%d sekund kvar</item>
|
||||
<item quantity="other">%d sekunder kvar</item>
|
||||
</plurals>
|
||||
<string name="compose_preview_image_description">Åtgärder för bild %s</string>
|
||||
<string name="pref_title_animate_gif_avatars">Animera profil gifar</string>
|
||||
|
@ -477,4 +476,8 @@
|
|||
<string name="pref_title_gradient_for_media">Visa färgglada gradienter för gömd media</string>
|
||||
<string name="action_unmute_domain">Ta bort tystad %s</string>
|
||||
<string name="action_unmute_desc">Ta bort tystad %s</string>
|
||||
<string name="pref_title_hide_top_toolbar">Dölj titeln i övre verktygsfältet</string>
|
||||
<string name="dialog_mute_hide_notifications">Dölj aviseringar</string>
|
||||
<string name="action_mute_notifications_desc">Tysta aviseringar från %s</string>
|
||||
<string name="action_unmute_notifications_desc">Aktivera aviseringar från %s</string>
|
||||
</resources>
|
|
@ -24,23 +24,22 @@
|
|||
<string name="button_back">ย้อนกลับ</string>
|
||||
<string name="button_continue">ต่อไป</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="other">%d วินาที</item>
|
||||
<item quantity="other">เหลืออีก %d วินาที</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="other">%d นาที</item>
|
||||
<item quantity="other">เหลืออีก %d นาที</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="other">%d ชั่วโมง</item>
|
||||
<item quantity="other">เหลืออีก %d ชั่วโมง</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="other">%d วัน</item>
|
||||
<item quantity="other">เหลืออีก %d วัน</item>
|
||||
</plurals>
|
||||
<string name="poll_ended_created">โพลที่คุณสร้างสิ้นสุดลงแล้ว</string>
|
||||
<string name="poll_ended_voted">โพลที่คุณโหวตสิ้นสุดลงแล้ว</string>
|
||||
<string name="poll_vote">โหวต</string>
|
||||
<string name="poll_info_closed">สิ้นสุดแล้ว</string>
|
||||
<string name="poll_info_time_absolute">จบที่ %s</string>
|
||||
<string name="poll_info_time_relative">เหลืออีก %s</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="other">%s คน</item>
|
||||
</plurals>
|
||||
|
@ -364,28 +363,28 @@
|
|||
<string name="action_bookmark">คั่นหน้า</string>
|
||||
<string name="action_favourite">ชื่นชอบ</string>
|
||||
<string name="action_unreblog">ลบบูสต์</string>
|
||||
<string name="action_reblog">บูสต์</string>
|
||||
<string name="action_reblog">ดัน</string>
|
||||
<string name="action_reply">ตอบกลับ</string>
|
||||
<string name="action_quick_reply">ตอบกลับด่วน</string>
|
||||
<string name="report_comment_hint">ความคิดเห็นเพิ่มเติม\?</string>
|
||||
<string name="report_username_format">รายงาน @%s</string>
|
||||
<string name="notification_follow_request_format">%s ต้องการติดตามคุณ</string>
|
||||
<string name="notification_follow_format">%s ได้ติดตามคุณ</string>
|
||||
<string name="notification_favourite_format">%s ได้ชื่นชอบ Toot คุณ</string>
|
||||
<string name="notification_reblog_format">%s ได้บูสต์ Toot คุณ</string>
|
||||
<string name="footer_empty">ไม่อะไรเลย ลากลงเพื่อรีเฟรช!</string>
|
||||
<string name="notification_favourite_format">%s ได้ชื่นชอบโพสต์ของคุณ</string>
|
||||
<string name="notification_reblog_format">%s ได้ดันโพสต์ของคุณ</string>
|
||||
<string name="footer_empty">ไม่มีอะไรเลย ลากลงเพื่อรีเฟรช!</string>
|
||||
<string name="message_empty">ไม่มีอะไร</string>
|
||||
<string name="status_content_show_less">ย่อ</string>
|
||||
<string name="status_content_show_more">ขยาย</string>
|
||||
<string name="status_content_warning_show_less">แสดงน้อยลง</string>
|
||||
<string name="status_content_warning_show_more">แสดงเพิ่มเติม</string>
|
||||
<string name="status_sensitive_media_directions">แตะเพื่อดู</string>
|
||||
<string name="status_media_hidden_title">สื่อที่ซ่อนไว้</string>
|
||||
<string name="status_media_hidden_title">ซ่อนสื่ออยู่</string>
|
||||
<string name="status_sensitive_media_title">เนื้อหาอ่อนไหว</string>
|
||||
<string name="status_boosted_format">%s ได้บูสต์</string>
|
||||
<string name="status_boosted_format">%s ได้ดัน</string>
|
||||
<string name="status_username_format">\@%s</string>
|
||||
<string name="title_licenses">สัญญาอนุญาต</string>
|
||||
<string name="title_scheduled_toot">Toot แบบกำหนดเวลา</string>
|
||||
<string name="title_scheduled_toot">โพสต์แบบกำหนดเวลา</string>
|
||||
<string name="title_edit_profile">แก้ไขโปรไฟล์</string>
|
||||
<string name="title_follow_requests">คำขอติดตาม</string>
|
||||
<string name="title_domain_mutes">โดเมนที่ซ่อนไว้</string>
|
||||
|
@ -399,15 +398,15 @@
|
|||
<string name="title_statuses">โพสต์</string>
|
||||
<string name="title_view_thread">เธรด</string>
|
||||
<string name="title_tab_preferences">แท็บ</string>
|
||||
<string name="title_direct_messages">ข้อความแบบไดเร็กต์</string>
|
||||
<string name="title_public_federated">สหพันธ์</string>
|
||||
<string name="title_public_local">ท้องถิ่น</string>
|
||||
<string name="title_direct_messages">ข้อความโดยตรง</string>
|
||||
<string name="title_public_federated">ที่ติดต่อกับภายนอก</string>
|
||||
<string name="title_public_local">ในเซิร์ฟเวอร์</string>
|
||||
<string name="title_notifications">แจ้งเตือน</string>
|
||||
<string name="title_home">หน้าหลัก</string>
|
||||
<string name="error_sender_account_gone">การส่ง Toot เกิดความผิดพลาด</string>
|
||||
<string name="error_sender_account_gone">การส่งโพสต์เกิดความผิดพลาด</string>
|
||||
<string name="error_media_upload_sending">อัปโหลดล้มเหลว</string>
|
||||
<string name="error_media_upload_image_or_video">ไม่สามารถแนบรูปภาพและวิดีทัศน์ในโพสต์เดียวกันได้</string>
|
||||
<string name="error_media_download_permission">ต้องมีสิทธิ์เขียนบนสื่อ</string>
|
||||
<string name="error_media_download_permission">ต้องมีสิทธิ์จัดเก็บสื่อ</string>
|
||||
<string name="error_media_upload_permission">ต้องมีสิทธิ์อ่านสื่อ</string>
|
||||
<string name="error_media_upload_opening">ไม่สามารถเปิดไฟล์ได้</string>
|
||||
<string name="error_media_upload_type">ไม่สามารถอัปโหลดไฟล์ประเภทนี้ได้</string>
|
||||
|
@ -434,7 +433,7 @@
|
|||
<string name="action_logout">ออกจากระบบ</string>
|
||||
<string name="title_saved_toot">ฉบับร่าง</string>
|
||||
<string name="title_favourites">ชื่นชอบ</string>
|
||||
<string name="error_failed_app_registration">การยืนยันตัวตนทางอิเล็กทรอนิกส์กับ Instance นั้นล้มเหลว</string>
|
||||
<string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string>
|
||||
<string name="link_whats_an_instance">Instance คือ\?</string>
|
||||
<string name="action_login">เข้าสู่ระบบด้วย Mastodon</string>
|
||||
<string name="poll_allow_multiple_choices">เลือกได้หลายตัวเลือก</string>
|
||||
|
@ -455,4 +454,5 @@
|
|||
<string name="action_unmute_notifications_desc">เลิกปิดเสียงการแจ้งเตือนจาก %s</string>
|
||||
<string name="dialog_mute_hide_notifications">ซ่อนการแจ้งเตือน</string>
|
||||
<string name="action_mute_notifications_desc">ปิดเสียงการแจ้งเตือนจาก %s</string>
|
||||
<string name="pref_title_hide_top_toolbar">ซ่อนหัวข้อของแถบเครื่องมือด้านบน</string>
|
||||
</resources>
|
|
@ -6,17 +6,17 @@
|
|||
<string name="error_invalid_domain">Girilen alan alanı geçersiz</string>
|
||||
<string name="error_failed_app_registration">Kimlik doğrulama başarısız oldu.</string>
|
||||
<string name="error_no_web_browser_found">Kullanılabilir web tarayıcısı bulunamadı.</string>
|
||||
<string name="error_authorization_unknown">Açıklanmayan kimlik doğrulama hata oluştu.</string>
|
||||
<string name="error_authorization_denied">Kimlik doğrulama reddedildi.</string>
|
||||
<string name="error_authorization_unknown">Tanımlanamayan bir yetkilendirme hatası oluştu.</string>
|
||||
<string name="error_authorization_denied">Yetkilendirme reddedildi.</string>
|
||||
<string name="error_retrieving_oauth_token">Giriş belirteci alınırken hata oluştu.</string>
|
||||
<string name="error_compose_character_limit">Durum çok uzun!</string>
|
||||
<string name="error_image_upload_size">Dosya 8 MB\'dan küçük olmalıdır.</string>
|
||||
<string name="error_video_upload_size">Video dosyaları 40 MB’dan küçük olmalıdır.</string>
|
||||
<string name="error_image_upload_size">Dosya 8 MB\'dan küçük olmalı.</string>
|
||||
<string name="error_video_upload_size">Video dosyaları 40 MB’dan küçük olmalı.</string>
|
||||
<string name="error_media_upload_type">Bu biçimdeki dosyalar yüklenmez.</string>
|
||||
<string name="error_media_upload_opening">Dosya açılamadı.</string>
|
||||
<string name="error_media_upload_permission">Medyayı okumak için izin gerekiyor.</string>
|
||||
<string name="error_media_download_permission">Medya kaydetme izni gerekiyor.</string>
|
||||
<string name="error_media_upload_image_or_video">Aynı iletiye hem video hem resim eklenemez.</string>
|
||||
<string name="error_media_upload_permission">Medya okuma izni gerekli.</string>
|
||||
<string name="error_media_download_permission">Medya kaydetme izni gerekli.</string>
|
||||
<string name="error_media_upload_image_or_video">Görüntüler ve videolar aynı duruma eklenemez.</string>
|
||||
<string name="error_media_upload_sending">Yükleme başarısız oldu.</string>
|
||||
<string name="error_sender_account_gone">Toot gönderilirken hata oluştu.</string>
|
||||
<string name="title_home">Ana sayfa</string>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<string name="title_public_federated">Birleşmiş</string>
|
||||
<string name="title_direct_messages">Direkt Mesajlar</string>
|
||||
<string name="title_tab_preferences">Sekmeler</string>
|
||||
<string name="title_view_thread">Dizi</string>
|
||||
<string name="title_view_thread">Toot</string>
|
||||
<string name="title_statuses">Gönderiler</string>
|
||||
<string name="title_statuses_with_replies">Yanıtlar ile</string>
|
||||
<string name="title_statuses_pinned">Sabitlenmiş</string>
|
||||
|
@ -49,8 +49,8 @@
|
|||
<string name="status_content_show_less">Daralt</string>
|
||||
<string name="message_empty">Burada hiçbir şey yok.</string>
|
||||
<string name="footer_empty">Henüz hiç ileti yoktur. Yenilemek için aşağıya çek!</string>
|
||||
<string name="notification_reblog_format">%s iletini yükseltti</string>
|
||||
<string name="notification_favourite_format">%s durumunu favorilerine ekledi</string>
|
||||
<string name="notification_reblog_format">%s tootunuzu boost etti</string>
|
||||
<string name="notification_favourite_format">%s tootunuzu favorilerine ekledi</string>
|
||||
<string name="notification_follow_format">%s seni takip etti</string>
|
||||
<string name="report_username_format">\@%s bildir</string>
|
||||
<string name="report_comment_hint">Daha fazla yorum?</string>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<string name="action_more">Daha fazla</string>
|
||||
<string name="action_compose">Oluştur</string>
|
||||
<string name="action_login">Mastodon ile giriş yap</string>
|
||||
<string name="action_logout">Çıkış Yap</string>
|
||||
<string name="action_logout">Oturumu Kapat</string>
|
||||
<string name="action_logout_confirm">Bu %1$s oturumu sonlandırmak istediğinizden emin misiniz\?</string>
|
||||
<string name="action_follow">Takip et</string>
|
||||
<string name="action_unfollow">Takibi bırak</string>
|
||||
|
@ -116,7 +116,7 @@
|
|||
<string name="confirmation_unmuted">Kullanıcının sesi açıldı</string>
|
||||
<string name="status_sent">İletildi!</string>
|
||||
<string name="status_sent_long">Yanıt başarıyla gönderildi.</string>
|
||||
<string name="hint_domain">Sunucu giriniz</string>
|
||||
<string name="hint_domain">Hangi örnek\?</string>
|
||||
<string name="hint_compose">Neler oluyor?</string>
|
||||
<string name="hint_content_warning">İçerik uyarı</string>
|
||||
<string name="hint_display_name">Görünen ad</string>
|
||||
|
@ -198,7 +198,7 @@
|
|||
<string name="notification_favourite_name">Favoriler</string>
|
||||
<string name="notification_favourite_description">Tootların favori olarak işaretlendiğinde</string>
|
||||
<string name="notification_mention_format">%s senden bahsetti</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s ve %4$d daha</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s ve %4$d diğer</string>
|
||||
<string name="notification_summary_medium">%1$s, %2$s ve %3$s</string>
|
||||
<string name="notification_summary_small">%1$s ve %2$s</string>
|
||||
<string name="notification_title_summary">%d yeni etkileşim</string>
|
||||
|
@ -250,13 +250,13 @@
|
|||
<string name="lock_account_label">Hesabı Kilitle</string>
|
||||
<string name="lock_account_label_description">Takipçileri elle onaylamanız gerekir</string>
|
||||
<string name="compose_save_draft">Taslaklara kaydedilsin mi\?</string>
|
||||
<string name="send_toot_notification_title">Durum gönderiliyor…</string>
|
||||
<string name="send_toot_notification_error_title">Durum gönderilirken hata oluştu</string>
|
||||
<string name="send_toot_notification_channel_name">Toot gönderiliyor</string>
|
||||
<string name="send_toot_notification_title">Toot gönderiliyor…</string>
|
||||
<string name="send_toot_notification_error_title">Toot gönderilirken hata oluştu</string>
|
||||
<string name="send_toot_notification_channel_name">Toot Gönderiliyor</string>
|
||||
<string name="send_toot_notification_cancel_title">Gönderme iptal edildi</string>
|
||||
<string name="send_toot_notification_saved_content">Tootun bir kopyası taslaklara kaydedildi</string>
|
||||
<string name="action_compose_shortcut">Oluştur</string>
|
||||
<string name="error_no_custom_emojis">%s sunucunuzun herhangi bir özel ifadeye sahip değil</string>
|
||||
<string name="error_no_custom_emojis">%s örneğinizin herhangi bir özel ifadesi yok</string>
|
||||
<string name="copy_to_clipboard_success">Panoya kopyalandı</string>
|
||||
<string name="emoji_style">İfade stili</string>
|
||||
<string name="system_default">Sistem varsayılanı</string>
|
||||
|
@ -327,7 +327,7 @@
|
|||
<string name="filter_dialog_remove_button">Kaldır</string>
|
||||
<string name="filter_dialog_update_button">Güncelle</string>
|
||||
<string name="filter_dialog_whole_word">Tüm dünya</string>
|
||||
<string name="filter_dialog_whole_word_description">Anahtar kelime veya kelime öbeği yalnızca alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır.</string>
|
||||
<string name="filter_dialog_whole_word_description">Bir anahtar kelime veya kelime öbeği sadece alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır</string>
|
||||
<string name="filter_add_description">Filtrelenecek ifade</string>
|
||||
<string name="error_create_list">Liste oluşturulamadı</string>
|
||||
<string name="error_rename_list">Liste yeniden adlandırılamadı</string>
|
||||
|
@ -361,29 +361,28 @@
|
|||
<string name="compose_preview_image_description">%s görüntüsü için eylemler</string>
|
||||
<string name="poll_info_format"> <!-- 15 oy • 1 saat kaldı --> %1$s • %2$s</string>
|
||||
<plurals name="poll_info_votes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%s oy</item>
|
||||
<item quantity="other">%s oy</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s kaldı</string>
|
||||
<string name="poll_info_closed">kapandı</string>
|
||||
<string name="poll_vote">Oy</string>
|
||||
<string name="poll_ended_voted">Oy verdiğin bir anket sona erdi</string>
|
||||
<string name="poll_ended_created">Oluşturduğun bir anket sona erdi</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%d gün kaldı</item>
|
||||
<item quantity="other">%d gün kaldı</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d saat</item>
|
||||
<item quantity="other">%d saat</item>
|
||||
<item quantity="one">%d saat kaldı</item>
|
||||
<item quantity="other">%d saat kaldı</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%d dakika kaldı</item>
|
||||
<item quantity="other">%d dakika kaldı</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d saniye</item>
|
||||
<item quantity="other">%d saniye</item>
|
||||
<item quantity="one">%d saniye kaldı</item>
|
||||
<item quantity="other">%d saniye kaldı</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Devam</string>
|
||||
<string name="button_back">Geri</string>
|
||||
|
@ -402,7 +401,7 @@
|
|||
<string name="action_open_media_n">#%d medyayı aç</string>
|
||||
<string name="title_bookmarks">Yer imleri</string>
|
||||
<string name="title_scheduled_toot">Zamanlanmış iletiler</string>
|
||||
<string name="action_bookmark">Yerimi</string>
|
||||
<string name="action_bookmark">Yer imi</string>
|
||||
<string name="action_edit">Düzenle</string>
|
||||
<string name="action_delete_and_redraft">Sil ve düzenle</string>
|
||||
<string name="action_view_bookmarks">Yer imleri</string>
|
||||
|
@ -433,8 +432,8 @@
|
|||
<string name="profile_badge_bot_text">Alt Metin</string>
|
||||
<string name="confirmation_domain_unmuted">%s alan adını gizleme</string>
|
||||
<string name="mute_domain_warning_dialog_ok">Alan adından her şeyi gizle</string>
|
||||
<string name="pref_title_alway_open_spoiler">Hassas içerikleri göster</string>
|
||||
<string name="poll_info_time_absolute">%s sona eriyor</string>
|
||||
<string name="pref_title_alway_open_spoiler">Her zaman içerik uyarılarıyla işaretlenmiş alanları genişlet</string>
|
||||
<string name="poll_info_time_absolute">%s içinde sona erecek</string>
|
||||
<string name="failed_report">Bildirilemedi</string>
|
||||
<string name="poll_new_choice_hint">Seçenek %d</string>
|
||||
<string name="post_lookup_error_format">%s gönderisi aranırken hata oluştu</string>
|
||||
|
@ -446,8 +445,8 @@
|
|||
<string name="pref_title_show_cards_in_timelines">Bağlantı önizlemelerini zaman çizelgesinde göster</string>
|
||||
<string name="pref_title_enable_swipe_for_tabs">Sekmeler arasında geçiş yapmak için kaydırma hareketini etkinleştir</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%s kişi</item>
|
||||
<item quantity="other">%s kişi</item>
|
||||
</plurals>
|
||||
<string name="add_hashtag_title">Hashtag ekle</string>
|
||||
<string name="notification_follow_request_description">Takip istekleri ile ilgili bildirimler</string>
|
||||
|
@ -462,12 +461,13 @@
|
|||
<string name="action_unmute_notifications_desc">%s kullanıcısından gelen bildirimleri yoksay</string>
|
||||
<string name="action_unmute_desc">%s sesini aç</string>
|
||||
<string name="notification_follow_request_format">%s seni takip etmek istiyor</string>
|
||||
<string name="error_audio_upload_size">Ses dosyaları 40 MB\'dan büyük olamaz.</string>
|
||||
<string name="error_audio_upload_size">Ses dosyaları 40 MB\'dan küçük olmalı.</string>
|
||||
<string name="action_unmute_conversation">Sohbetin sesini aç</string>
|
||||
<string name="pref_title_notification_filter_follow_requests">takip istendi</string>
|
||||
<string name="action_mute_conversation">Sohbeti sessize al</string>
|
||||
<string name="pref_main_nav_position">Ana gezinti konumu</string>
|
||||
<string name="pref_title_gradient_for_media">Gizli medya için renkli gradyanlar göster</string>
|
||||
<string name="error_failed_set_caption">Başlık ayarlama başarısız oldu</string>
|
||||
<string name="error_failed_set_caption">Başlık ayarlanamadı</string>
|
||||
<string name="warning_scheduling_interval">Mastodon\'un minimum 5 dakikalık zamanlama aralığı vardır.</string>
|
||||
<string name="pref_title_hide_top_toolbar">Üst araç çubuğunun başlığını gizle</string>
|
||||
</resources>
|
||||
|
|
|
@ -119,4 +119,32 @@
|
|||
<string name="title_blocks">Заблоковані користувачі</string>
|
||||
<string name="notification_favourite_name">Вподобане</string>
|
||||
<string name="notification_follow_request_name">Запити на підписку</string>
|
||||
<string name="action_unmute_desc">Розблокувати %s</string>
|
||||
<string name="action_unmute">Відмінити приглушення</string>
|
||||
<string name="action_view_domain_mutes">Приховані домени</string>
|
||||
<string name="action_view_mutes">Список глушіння</string>
|
||||
<string name="action_send_public">ТООТ!</string>
|
||||
<string name="action_send">ТООТ</string>
|
||||
<string name="action_show_reblogs">Показати просування</string>
|
||||
<string name="action_hide_reblogs">Приховати просування</string>
|
||||
<string name="action_more">Розгорнути</string>
|
||||
<string name="action_unreblog">Забртаи просунення</string>
|
||||
<string name="action_reblog">Просунути</string>
|
||||
<string name="notification_favourite_format">%s сподабався ваш статус</string>
|
||||
<string name="notification_reblog_format">%s просунув(ла) ваш статус</string>
|
||||
<string name="status_content_show_less">Згорнути</string>
|
||||
<string name="status_content_show_more">Розгорнути</string>
|
||||
<string name="status_sensitive_media_title">Чутливий вміст</string>
|
||||
<string name="status_boosted_format">%s Просунув(ла)</string>
|
||||
<string name="title_domain_mutes">Приховані домени</string>
|
||||
<string name="title_mutes">Заглушені користувачі</string>
|
||||
<string name="title_statuses">Дописи</string>
|
||||
<string name="title_view_thread">Поширити</string>
|
||||
<string name="title_tab_preferences">Вкладки</string>
|
||||
<string name="error_media_upload_sending">Завантаження не вдалося.</string>
|
||||
<string name="error_retrieving_oauth_token">Не вдалося отримати токін авторизації.</string>
|
||||
<string name="error_authorization_denied">Авторизація була відхилина.</string>
|
||||
<string name="error_authorization_unknown">Сталася помилка неопізнаної авторизації.</string>
|
||||
<string name="error_failed_app_registration">Помилка входу з цією інстанцією.</string>
|
||||
<string name="error_invalid_domain">Введено недійсний домен</string>
|
||||
</resources>
|
|
@ -48,7 +48,7 @@
|
|||
<string name="action_reset_schedule">Làm tươi</string>
|
||||
<string name="action_search">Tìm kiếm</string>
|
||||
<string name="action_edit_profile">Trang cá nhân</string>
|
||||
<string name="action_view_account_preferences">Tài khoản</string>
|
||||
<string name="action_view_account_preferences">Riêng bạn</string>
|
||||
<string name="action_view_preferences">Cài đặt</string>
|
||||
<string name="action_logout">Đăng xuất</string>
|
||||
<string name="button_done">Xong</string>
|
||||
|
@ -67,11 +67,11 @@
|
|||
<string name="dialog_message_uploading_media">Đang tải…</string>
|
||||
<string name="dialog_title_finishing_media_upload">Đã tải xong tập tin</string>
|
||||
<string name="dialog_whats_an_instance">Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và <a href="https://instances.social">nhiều hơn nữa!</a>
|
||||
\n
|
||||
\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó.
|
||||
\n
|
||||
\nMột máy chủ là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn cũng có thể giao tiếp và theo dõi với mọi người trên các máy chủ khác một cách dễ dàng.
|
||||
\n
|
||||
\n
|
||||
\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó.
|
||||
\n
|
||||
\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng.
|
||||
\n
|
||||
\nTham khảo thêm tại <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
|
||||
<string name="login_connection">Đang kết nối…</string>
|
||||
<string name="label_header">Ảnh bìa</string>
|
||||
|
@ -207,10 +207,10 @@
|
|||
<string name="pref_title_status_filter">Lọc bảng tin</string>
|
||||
<string name="pref_title_gradient_for_media">Che mờ nội dung nhạy cảm</string>
|
||||
<string name="pref_title_animate_gif_avatars">Hiện ảnh đại diện GIF</string>
|
||||
<string name="pref_title_bot_overlay">Hiện tút từ tài khoản Bot</string>
|
||||
<string name="pref_title_bot_overlay">Hiện icon cho tài khoản Bot</string>
|
||||
<string name="pref_title_language">Ngôn ngữ</string>
|
||||
<string name="pref_title_hide_follow_button">Ẩn nút viết tút khi xem bảng tin</string>
|
||||
<string name="pref_title_custom_tabs">Sử dụng tab Chrome</string>
|
||||
<string name="pref_title_custom_tabs">Mở luôn trong app</string>
|
||||
<string name="pref_title_browser_settings">Trình duyệt</string>
|
||||
<string name="app_theme_system">Mặc định của thiết bị</string>
|
||||
<string name="app_theme_auto">Tự động khi trời tối</string>
|
||||
|
@ -259,7 +259,7 @@
|
|||
<string name="pref_main_nav_position_option_top">Trên màn hình</string>
|
||||
<string name="pref_main_nav_position">Vị trí menu</string>
|
||||
<string name="pref_failed_to_sync">Đồng bộ hoá thất bại</string>
|
||||
<string name="pref_publishing">Đăng (đồng bộ với server)</string>
|
||||
<string name="pref_publishing">Đăng (đồng bộ với máy chủ)</string>
|
||||
<string name="pref_default_media_sensitivity">Luôn đánh dấu nội dung là nhạy cảm</string>
|
||||
<string name="pref_default_post_privacy">Trạng thái tút mặc định</string>
|
||||
<string name="pref_title_http_proxy_server">HTTP proxy server</string>
|
||||
|
@ -298,7 +298,7 @@
|
|||
<string name="abbreviated_hours_ago">%d giờ</string>
|
||||
<string name="abbreviated_days_ago">%d ngày</string>
|
||||
<string name="abbreviated_years_ago">%d năm</string>
|
||||
<string name="abbreviated_in_seconds">in %ds</string>
|
||||
<string name="abbreviated_in_seconds">%ds</string>
|
||||
<string name="abbreviated_in_minutes">%d phút</string>
|
||||
<string name="abbreviated_in_hours">in %d giờ</string>
|
||||
<string name="abbreviated_in_days">in %d ngày</string>
|
||||
|
@ -326,35 +326,34 @@
|
|||
<string name="poll_duration_30_min">30 phút</string>
|
||||
<string name="poll_duration_5_min">5 phút</string>
|
||||
<string name="create_poll_title">Bình chọn</string>
|
||||
<string name="pref_title_enable_swipe_for_tabs">Sử dụng thao tác cử chỉ để chuyển qua lại giữa các tab</string>
|
||||
<string name="pref_title_enable_swipe_for_tabs">Vuốt để chuyển qua lại giữa các tab</string>
|
||||
<string name="pref_title_show_notifications_filter">Hiện bộ lọc thông báo</string>
|
||||
<string name="failed_search">Không thể tìm thấy</string>
|
||||
<string name="title_accounts">Người</string>
|
||||
<string name="report_description_remote_instance">Tài khoản này thuộc máy chủ khác. Gửi báo cáo ẩn danh\?</string>
|
||||
<string name="report_description_1">Báo cáo này sẽ được gửi tới kiểm duyệt viên máy chủ của bạn. Hãy cung cấp nội dung vì sao bạn báo cáo người này bên dưới:</string>
|
||||
<string name="report_description_remote_instance">Tài khoản này thuộc máy chủ khác. Gửi luôn cho máy chủ đó\?</string>
|
||||
<string name="report_description_1">Báo cáo này sẽ được gửi tới kiểm duyệt viên. Hãy cho biết lý do vì sao bạn báo cáo người này bên dưới:</string>
|
||||
<string name="failed_fetch_statuses">Không tải được tút</string>
|
||||
<string name="failed_report">Báo cáo thất bại</string>
|
||||
<string name="report_remote_instance">Dời sang %s</string>
|
||||
<string name="report_remote_instance">Gửi cho %s</string>
|
||||
<string name="hint_additional_info">Thêm ghi chú</string>
|
||||
<string name="report_sent_success">Đã gửi báo cáo @%s</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="other">%d giây</item>
|
||||
<item quantity="other">%d giây nữa kết thúc</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="other">%d phút</item>
|
||||
<item quantity="other">%d phút nữa kết thúc</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="other">%d giờ</item>
|
||||
<item quantity="other">%d giờ nữa kết thúc</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="other">%d ngày</item>
|
||||
<item quantity="other">%d ngày nữa kết thúc</item>
|
||||
</plurals>
|
||||
<string name="poll_ended_created">Cuộc bình chọn bạn tạo đã kết thúc</string>
|
||||
<string name="poll_ended_voted">Một cuộc bình chọn mà bạn tham gia đã kết thúc</string>
|
||||
<string name="poll_ended_voted">Cuộc bình chọn bạn tham gia đã kết thúc</string>
|
||||
<string name="poll_vote">Bình chọn</string>
|
||||
<string name="poll_info_closed">Kết thúc</string>
|
||||
<string name="poll_info_time_absolute">kết thúc lúc %s</string>
|
||||
<string name="poll_info_time_relative">%s nữa kết thúc</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="other">%s người</item>
|
||||
</plurals>
|
||||
|
@ -371,7 +370,7 @@
|
|||
<string name="list">Danh sách</string>
|
||||
<string name="select_list_title">Chọn danh sách</string>
|
||||
<string name="hashtags">Hashtag</string>
|
||||
<string name="edit_hashtag_hint">Hashtag mà không #</string>
|
||||
<string name="edit_hashtag_hint">Không cần dấu #</string>
|
||||
<string name="add_hashtag_title">Thêm hashtag</string>
|
||||
<string name="hint_list_name">Tên danh sách</string>
|
||||
<string name="description_poll">Những lựa chọn: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
|
@ -409,7 +408,7 @@
|
|||
<string name="license_apache_2">Licensed under the Apache License (sao chép bên dưới)</string>
|
||||
<string name="license_description">Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:</string>
|
||||
<string name="unreblog_private">Hủy chia sẻ</string>
|
||||
<string name="reblog_private">Chia sẻ với người đăng</string>
|
||||
<string name="reblog_private">Chia sẻ công khai</string>
|
||||
<string name="account_moved_description">%1$s đã dời sang:</string>
|
||||
<string name="profile_badge_bot_text">Tài khoản Bot</string>
|
||||
<string name="download_failed">Tải về thất bại</string>
|
||||
|
@ -448,12 +447,12 @@
|
|||
<string name="add_account_description">Thêm tài khoản Mastodon</string>
|
||||
<string name="add_account_name">Thêm tài khoản</string>
|
||||
<string name="filter_add_description">Thêm mô tả</string>
|
||||
<string name="filter_dialog_whole_word_description">Khi từ khóa là chữ và số, nó sẽ chỉ được áp dụng nếu nó phù hợp với toàn bộ từ</string>
|
||||
<string name="filter_dialog_whole_word_description">Bất kể từ khóa là từ hoặc cụm từ, những kết quả hiện ra sẽ giống hệt như bạn nhập</string>
|
||||
<string name="description_status_media">Media: %s</string>
|
||||
<string name="dialog_mute_hide_notifications">Ẩn thông báo</string>
|
||||
<string name="action_mute_notifications_desc">Ẩn thông báo từ %s</string>
|
||||
<string name="action_unmute_notifications_desc">Bỏ ẩn thông báo từ %s</string>
|
||||
<string name="action_unmute_desc">Bỏ ẩn %s</string>
|
||||
<string name="action_unmute_domain">Bỏ ẩn %s</string>
|
||||
<string name="pref_title_hide_top_toolbar">Ẩn tên tab</string>
|
||||
<string name="pref_title_hide_top_toolbar">Ẩn tiêu đề tab</string>
|
||||
</resources>
|
|
@ -1,24 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_generic">应用程序出现异常。</string>
|
||||
<string name="error_generic">应用程序出现异常</string>
|
||||
<string name="error_network">网络请求出错,请检查互联网连接并重试!</string>
|
||||
<string name="error_empty">内容不能为空。</string>
|
||||
<string name="error_empty">内容不能为空</string>
|
||||
<string name="error_invalid_domain">该域名无效</string>
|
||||
<string name="error_failed_app_registration">无法连接此服务器。</string>
|
||||
<string name="error_no_web_browser_found">没有可用的浏览器。</string>
|
||||
<string name="error_authorization_unknown">认证过程出现未知错误。</string>
|
||||
<string name="error_authorization_denied">授权被拒绝。</string>
|
||||
<string name="error_retrieving_oauth_token">无法获取登录信息。</string>
|
||||
<string name="error_failed_app_registration">无法连接此服务器</string>
|
||||
<string name="error_no_web_browser_found">没有可用的浏览器</string>
|
||||
<string name="error_authorization_unknown">认证过程出现未知错误</string>
|
||||
<string name="error_authorization_denied">授权被拒绝</string>
|
||||
<string name="error_retrieving_oauth_token">无法获取登录信息</string>
|
||||
<string name="error_compose_character_limit">嘟文太长了!</string>
|
||||
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||
<string name="error_video_upload_size">视频文件大小限制 40MB。</string>
|
||||
<string name="error_media_upload_type">无法上传此类型的文件。</string>
|
||||
<string name="error_media_upload_opening">此文件无法打开。</string>
|
||||
<string name="error_media_upload_permission">需要授予 Yuito 读取媒体文件的权限。</string>
|
||||
<string name="error_media_download_permission">需要授予 Yuito 写入存储空间的权限。</string>
|
||||
<string name="error_media_upload_image_or_video">无法在嘟文中同时插入视频和图片。</string>
|
||||
<string name="error_media_upload_sending">媒体文件上传失败。</string>
|
||||
<string name="error_sender_account_gone">嘟文发送时出错。</string>
|
||||
<string name="error_image_upload_size">文件大小限制 8MB</string>
|
||||
<string name="error_video_upload_size">视频文件大小限制 40MB</string>
|
||||
<string name="error_media_upload_type">无法上传此类型的文件</string>
|
||||
<string name="error_media_upload_opening">此文件无法打开</string>
|
||||
<string name="error_media_upload_permission">需要授予 Yuito 读取媒体文件的权限</string>
|
||||
<string name="error_media_download_permission">需要授予 Yuito 写入存储空间的权限</string>
|
||||
<string name="error_media_upload_image_or_video">无法在嘟文中同时插入视频和图片</string>
|
||||
<string name="error_media_upload_sending">媒体文件上传失败</string>
|
||||
<string name="error_sender_account_gone">嘟文发送时出错</string>
|
||||
<string name="title_home">主页</string>
|
||||
<string name="title_notifications">通知</string>
|
||||
<string name="title_public_local">本站时间轴</string>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<string name="status_content_warning_show_less">折叠内容</string>
|
||||
<string name="status_content_show_more">展开</string>
|
||||
<string name="status_content_show_less">折叠</string>
|
||||
<string name="message_empty">还没有内容。</string>
|
||||
<string name="message_empty">还没有内容</string>
|
||||
<string name="footer_empty">还没有内容,向下拉动即可刷新!</string>
|
||||
<string name="notification_reblog_format">%s 转嘟了你的嘟文</string>
|
||||
<string name="notification_favourite_format">%s 收藏了你的嘟文</string>
|
||||
|
@ -94,7 +94,7 @@
|
|||
<string name="action_unmute">取消隐藏</string>
|
||||
<string name="action_mention">提及</string>
|
||||
<string name="action_hide_media">隐藏媒体文件</string>
|
||||
<string name="action_open_drawer">打开应用抽屉</string>
|
||||
<string name="action_open_drawer">打开菜单</string>
|
||||
<string name="action_save">保存</string>
|
||||
<string name="action_edit_profile">编辑个人资料</string>
|
||||
<string name="action_edit_own_profile">编辑</string>
|
||||
|
@ -130,7 +130,7 @@
|
|||
<string name="confirmation_unblocked">已解除屏蔽</string>
|
||||
<string name="confirmation_unmuted">已取消隐藏</string>
|
||||
<string name="status_sent">已发送!</string>
|
||||
<string name="status_sent_long">成功发送回复。</string>
|
||||
<string name="status_sent_long">成功发送回复</string>
|
||||
<string name="hint_domain">域名</string>
|
||||
<string name="hint_compose">有什么新鲜事?</string>
|
||||
<string name="hint_content_warning">内容提醒</string>
|
||||
|
@ -376,13 +376,10 @@
|
|||
<string name="compose_shortcut_short_label">发表嘟文</string>
|
||||
<string name="pref_title_bot_overlay">显示机器人标志</string>
|
||||
<string name="notification_clear_text">你确定要永久清空通知列表吗?</string>
|
||||
<string name="poll_info_format">
|
||||
<!-- 15 votes • 1 hour left -->
|
||||
%1$s • %2$s</string>
|
||||
<string name="poll_info_format"> <!-- 15 票 • 1 小时剩余 --> %1$s • %2$s</string>
|
||||
<plurals name="poll_info_votes">
|
||||
<item quantity="other">%s 次投票</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">剩余 %s</string>
|
||||
<string name="poll_info_time_absolute">%s 结束</string>
|
||||
<string name="poll_info_closed">已结束</string>
|
||||
<string name="poll_vote">投票</string>
|
||||
|
@ -390,19 +387,19 @@
|
|||
<string name="poll_ended_created">你创建的投票已结束</string>
|
||||
<!--These are for timestamps on polls -->
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="other">%d 天</item>
|
||||
<item quantity="other">剩余 %d 天</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="other">%d 小时</item>
|
||||
<item quantity="other">剩余 %d 小时</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="other">%d 分钟</item>
|
||||
<item quantity="other">剩余 %d 分钟</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="other">%d 秒</item>
|
||||
<item quantity="other">剩余 %d 秒</item>
|
||||
</plurals>
|
||||
<string name="action_reset_schedule">重置</string>
|
||||
<string name="error_audio_upload_size">音频文件大小必须小于40M。</string>
|
||||
<string name="error_audio_upload_size">音频文件大小限制 40M</string>
|
||||
<string name="title_bookmarks">书签</string>
|
||||
<string name="title_domain_mutes">隐藏的域名</string>
|
||||
<string name="title_scheduled_toot">定时嘟文</string>
|
||||
|
@ -412,7 +409,7 @@
|
|||
<string name="action_view_domain_mutes">隐藏的域名</string>
|
||||
<string name="action_add_poll">新增投票</string>
|
||||
<string name="action_access_scheduled_toot">定时嘟文</string>
|
||||
<string name="action_schedule_toot">预订嘟文</string>
|
||||
<string name="action_schedule_toot">定时嘟文</string>
|
||||
<string name="confirmation_domain_unmuted">%s 已取消隐藏</string>
|
||||
<string name="mute_domain_warning_dialog_ok">隐藏来自该域名的所有嘟文</string>
|
||||
<string name="pref_title_animate_gif_avatars">动画GIF头像</string>
|
||||
|
@ -482,6 +479,6 @@
|
|||
<string name="pref_title_hide_top_toolbar">隐藏顶部工具栏标题</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s 人</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue