Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-11-20 13:21:21 +09:00
commit 171f69a35d
118 changed files with 1846 additions and 962 deletions

View File

@ -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'
}

View File

@ -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>

View File

@ -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()

View File

@ -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 -> {

View File

@ -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

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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)
}
}

View File

@ -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.")
}
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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)

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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
)
)

View File

@ -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);
}
}
}
}

View File

@ -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()
}

View File

@ -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"
}
}

View File

@ -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),

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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() {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
})
})
}
}

View File

@ -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?) {

View File

@ -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?) {

View File

@ -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,

View File

@ -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() {

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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?
)
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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
)

View File

@ -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
)

View File

@ -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)
}

View File

@ -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()));
}

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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>
}

View File

@ -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> {

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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"
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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() {

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 lanimation des avatars</string>
<string name="compose_preview_image_description">Actions pour limage %s</string>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">Sacaba 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 quavè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 limatge %s</string>
<string name="pref_title_animate_gif_avatars">Activar lanimacion dels avatars</string>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">ı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 MBdan 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 MBdan 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ı</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>

View File

@ -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>

View File

@ -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>

View File

@ -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