1
0
mirror of https://github.com/TwidereProject/Twidere-Android synced 2025-02-07 15:28:51 +01:00

implementing new dm

This commit is contained in:
Mariotaku Lee 2017-02-09 21:43:51 +08:00
parent fcaed05c23
commit 6fc10eed30
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
20 changed files with 331 additions and 72 deletions

View File

@ -150,7 +150,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
String QUERY_PARAM_NAME = "name";
String QUERY_PARAM_FINISH_ONLY = "finish_only";
String QUERY_PARAM_NEW_ITEMS_COUNT = "new_items_count";
String QUERY_PARAM_RECIPIENT_ID = "recipient_id";
String QUERY_PARAM_CONVERSATION_ID = "conversation_id";
String QUERY_PARAM_READ_POSITION = "param_read_position";
String QUERY_PARAM_READ_POSITIONS = "param_read_positions";
String QUERY_PARAM_LIMIT = "limit";

View File

@ -44,7 +44,7 @@ public class ParcelableMessage {
@JsonField(name = "type")
@CursorField(Messages.MESSAGE_TYPE)
@Type
@MessageType
public String message_type;
@JsonField(name = "timestamp")
@ -123,8 +123,8 @@ public class ParcelableMessage {
}
@StringDef({Type.TEXT, Type.STICKER})
public @interface Type {
@StringDef({MessageType.TEXT, MessageType.STICKER})
public @interface MessageType {
String TEXT = "text";
String STICKER = "sticker";
}

View File

@ -1,5 +1,7 @@
package org.mariotaku.twidere.model;
import android.support.annotation.StringDef;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableNoThanks;
@ -27,12 +29,21 @@ public class ParcelableMessageConversation {
@JsonField(name = "account_key")
@CursorField(value = Conversations.ACCOUNT_KEY, converter = UserKeyCursorFieldConverter.class)
public UserKey account_key;
@JsonField(name = "account_color")
@CursorField(Conversations.ACCOUNT_COLOR)
public int account_color;
@JsonField(name = "conversation_id")
@CursorField(Conversations.CONVERSATION_ID)
public String id;
@JsonField(name = "type")
@ConversationType
@JsonField(name = "conversation_type")
@CursorField(Conversations.CONVERSATION_TYPE)
public String conversation_type;
@ParcelableMessage.MessageType
@JsonField(name = "message_type")
@CursorField(Conversations.MESSAGE_TYPE)
public String message_type;
@ -90,15 +101,27 @@ public class ParcelableMessageConversation {
"_id=" + _id +
", account_key=" + account_key +
", id='" + id + '\'' +
", conversation_type='" + conversation_type + '\'' +
", message_type='" + message_type + '\'' +
", message_timestamp=" + message_timestamp +
", local_timestamp=" + local_timestamp +
", text_unescaped='" + text_unescaped + '\'' +
", media=" + Arrays.toString(media) +
", spans=" + Arrays.toString(spans) +
", extras=" + extras +
", internalExtras=" + internalExtras +
", participants=" + Arrays.toString(participants) +
", sender_key=" + sender_key +
", recipient_key=" + recipient_key +
", is_outgoing=" + is_outgoing +
", request_cursor='" + request_cursor + '\'' +
'}';
}
@StringDef({ConversationType.ONE_TO_ONE, ConversationType.GROUP})
public @interface ConversationType {
String ONE_TO_ONE = "one_to_one";
String GROUP = "group";
}
}

View File

@ -377,7 +377,9 @@ public interface TwidereDataStore {
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
interface Conversations extends BaseColumns, AccountSupportColumns {
String ACCOUNT_COLOR = "account_color";
String CONVERSATION_ID = "conversation_id";
String CONVERSATION_TYPE = "conversation_type";
String MESSAGE_TYPE = "message_type";
String MESSAGE_TIMESTAMP = "message_timestamp";
String LOCAL_TIMESTAMP = "local_timestamp";

View File

@ -34,7 +34,7 @@ import static org.mariotaku.twidere.annotation.PreferenceType.STRING;
public interface Constants extends TwidereConstants {
String DATABASES_NAME = "twidere.sqlite";
int DATABASES_VERSION = 167;
int DATABASES_VERSION = 169;
int EXTRA_FEATURES_NOTICE_VERSION = 0;

View File

@ -15,7 +15,6 @@ import org.mariotaku.twidere.annotation.CustomTabType;
import org.mariotaku.twidere.fragment.CustomTabsFragment.TabEditorDialogFragment;
import org.mariotaku.twidere.model.AccountDetails;
import org.mariotaku.twidere.model.Tab;
import org.mariotaku.twidere.model.tab.impl.DMTabConfiguration;
import org.mariotaku.twidere.model.tab.impl.FavoriteTimelineTabConfiguration;
import org.mariotaku.twidere.model.tab.impl.HomeTabConfiguration;
import org.mariotaku.twidere.model.tab.impl.InteractionsTabConfiguration;
@ -112,8 +111,6 @@ public abstract class TabConfiguration {
return new InteractionsTabConfiguration();
case CustomTabType.DIRECT_MESSAGES:
return new MessagesTabConfiguration();
case CustomTabType.DIRECT_MESSAGES_NEXT:
return new DMTabConfiguration();
case CustomTabType.LIST_TIMELINE:
return new UserListTimelineTabConfiguration();
case CustomTabType.FAVORITES:

View File

@ -1,40 +0,0 @@
package org.mariotaku.twidere.model.tab.impl;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.fragment.MessagesEntriesFragment;
import org.mariotaku.twidere.model.tab.DrawableHolder;
import org.mariotaku.twidere.model.tab.StringHolder;
import org.mariotaku.twidere.model.tab.TabConfiguration;
/**
* Created by mariotaku on 2016/11/27.
*/
public class DMTabConfiguration extends TabConfiguration {
@NonNull
@Override
public StringHolder getName() {
return StringHolder.resource(R.string.direct_messages_next);
}
@NonNull
@Override
public DrawableHolder getIcon() {
return DrawableHolder.Builtin.MESSAGE;
}
@AccountFlags
@Override
public int getAccountFlags() {
return FLAG_HAS_ACCOUNT | FLAG_ACCOUNT_MULTIPLE | FLAG_ACCOUNT_MUTABLE;
}
@NonNull
@Override
public Class<? extends Fragment> getFragmentClass() {
return MessagesEntriesFragment.class;
}
}

View File

@ -617,7 +617,7 @@ class LinkHandlerActivity : BaseActivity(), SystemWindowsInsetsCallback, IContro
LINK_ID_DIRECT_MESSAGES_CONVERSATION -> {
fragment = MessagesConversationFragment()
isAccountIdRequired = false
val paramRecipientId = uri.getQueryParameter(QUERY_PARAM_RECIPIENT_ID)
val paramRecipientId = uri.getQueryParameter(QUERY_PARAM_CONVERSATION_ID)
val paramScreenName = uri.getQueryParameter(QUERY_PARAM_SCREEN_NAME)
if (paramRecipientId != null) {
args.putString(EXTRA_RECIPIENT_ID, paramRecipientId)

View File

@ -24,7 +24,7 @@ class ListParcelableStatusesAdapter(context: Context) : ParcelableStatusesAdapte
fun createStatusViewHolder(adapter: IStatusesAdapter<*>,
inflater: LayoutInflater, parent: ViewGroup): StatusViewHolder {
val view = inflater.inflate(R.layout.list_item_status, parent, false)
val view = inflater.inflate(StatusViewHolder.layoutResource, parent, false)
val holder = StatusViewHolder(adapter, view)
holder.setOnClickListeners()
holder.setupViewOptions()

View File

@ -0,0 +1,73 @@
package org.mariotaku.twidere.adapter
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
import org.mariotaku.twidere.model.ItemCounts
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.view.holder.MessageConversationViewHolder
/**
* Created by mariotaku on 2017/2/9.
*/
class MessagesConversationsAdapter(context: Context) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context),
IItemCountsAdapter {
override val itemCounts: ItemCounts = ItemCounts(1)
var conversations: List<ParcelableMessageConversation>? = null
set(value) {
field = value
notifyDataSetChanged()
}
var drawAccountColors: Boolean = false
set(value) {
field = value
notifyDataSetChanged()
}
var listener: MessageConversationClickListener? = null
override fun getItemCount(): Int {
itemCounts[0] = conversations?.size ?: 0
return itemCounts.itemCount
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
ITEM_TYPE_MESSAGE_ENTRY -> {
val conversation = getConversation(position)!!
(holder as MessageConversationViewHolder).display(conversation)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(MessageConversationViewHolder.layoutResource, parent, false)
return MessageConversationViewHolder(itemView, this)
}
override fun getItemViewType(position: Int): Int {
return ITEM_TYPE_MESSAGE_ENTRY
}
fun getConversation(position: Int): ParcelableMessageConversation? {
return conversations?.get(position - itemCounts.getItemStartPosition(0))
}
interface MessageConversationClickListener {
fun onProfileImageClick(position: Int)
fun onConversationClick(position: Int)
}
companion object {
const val ITEM_TYPE_MESSAGE_ENTRY = 1
}
}

View File

@ -1,11 +1,17 @@
package org.mariotaku.twidere.extension.model
import android.content.Context
import org.mariotaku.twidere.R
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableMessage
import org.mariotaku.twidere.model.ParcelableMessage.MessageType
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationType
import org.mariotaku.twidere.model.ParcelableUser
fun ParcelableMessageConversation.setFrom(message: ParcelableMessage) {
account_key = message.account_key
id = message.conversation_id
fun ParcelableMessageConversation.setFrom(message: ParcelableMessage, details: AccountDetails) {
account_key = details.key
account_color = details.color
message_type = message.message_type
message_timestamp = message.message_timestamp
local_timestamp = message.local_timestamp
@ -21,3 +27,28 @@ fun ParcelableMessageConversation.setFrom(message: ParcelableMessage) {
val ParcelableMessageConversation.timestamp: Long
get() = if (message_timestamp > 0) message_timestamp else local_timestamp
fun ParcelableMessageConversation.getConversationName(context: Context): Pair<String, String?> {
if (conversation_type == ConversationType.ONE_TO_ONE) {
val user = this.user ?: return Pair(context.getString(R.string.direct_messages), null)
return Pair(user.name, user.screen_name)
}
return Pair(participants.joinToString(separator = ", ") {
it.name
}, null)
}
fun ParcelableMessageConversation.getSummaryText(context: Context): String {
when (message_type) {
MessageType.STICKER -> {
return context.getString(R.string.message_summary_type_sticker)
}
}
return text_unescaped
}
val ParcelableMessageConversation.user: ParcelableUser?
get() {
val userKey = if (is_outgoing) recipient_key else sender_key
return participants.firstOrNull { it.key == userKey }
}

View File

@ -105,7 +105,6 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
return CursorStatusesBusCallback()
}
private fun showContentOrError() {
val accountKeys = this.accountKeys
if (adapter.itemCount > 0) {

View File

@ -1,8 +1,100 @@
package org.mariotaku.twidere.fragment
import android.support.v4.app.Fragment
import android.content.Context
import android.os.Bundle
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import org.mariotaku.kpreferences.get
import org.mariotaku.sqliteqb.library.OrderBy
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesConversationsAdapter
import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.extension.model.user
import org.mariotaku.twidere.loader.ObjectCursorLoader
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableMessageConversationCursorIndices
import org.mariotaku.twidere.model.SimpleRefreshTaskParam
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.ErrorInfoStore
import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.util.Utils
/**
* Created by mariotaku on 16/3/28.
*/
class MessagesEntriesFragment : Fragment()
class MessagesEntriesFragment : AbsContentListRecyclerViewFragment<MessagesConversationsAdapter>(),
LoaderManager.LoaderCallbacks<List<ParcelableMessageConversation>?>, MessagesConversationsAdapter.MessageConversationClickListener {
private val accountKeys: Array<UserKey>
get() = Utils.getAccountKeys(context, arguments) ?: DataStoreUtils.getActivatedAccountKeys(context)
private val errorInfoKey: String = ErrorInfoStore.KEY_DIRECT_MESSAGES
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.listener = this
loaderManager.initLoader(0, null, this)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ParcelableMessageConversation>?> {
val loader = ObjectCursorLoader(context, ParcelableMessageConversationCursorIndices::class.java)
loader.uri = Conversations.CONTENT_URI
loader.projection = Conversations.COLUMNS
loader.sortOrder = OrderBy(Conversations.LOCAL_TIMESTAMP, false).sql
return loader
}
override fun onLoaderReset(loader: Loader<List<ParcelableMessageConversation>?>?) {
adapter.conversations = null
}
override fun onLoadFinished(loader: Loader<List<ParcelableMessageConversation>?>?, data: List<ParcelableMessageConversation>?) {
adapter.conversations = data
adapter.drawAccountColors = accountKeys.size > 1
showContentOrError()
}
override fun onCreateAdapter(context: Context): MessagesConversationsAdapter {
return MessagesConversationsAdapter(context)
}
override fun triggerRefresh(): Boolean {
super.triggerRefresh()
twitterWrapper.getMessagesAsync(object : SimpleRefreshTaskParam() {
override fun getAccountKeysWorker(): Array<UserKey> {
return this@MessagesEntriesFragment.accountKeys
}
})
return true
}
override fun onConversationClick(position: Int) {
val conversation = adapter.getConversation(position) ?: return
IntentUtils.openMessageConversation(context, conversation.account_key, conversation.id)
}
override fun onProfileImageClick(position: Int) {
val conversation = adapter.getConversation(position) ?: return
val user = conversation.user ?: return
IntentUtils.openUserProfile(context, user, preferences[newDocumentApiKey])
}
private fun showContentOrError() {
val accountKeys = this.accountKeys
if (adapter.itemCount > 0) {
showContent()
} else if (accountKeys.isNotEmpty()) {
val errorInfo = ErrorInfoStore.getErrorInfo(context,
errorInfoStore[errorInfoKey, accountKeys[0]])
if (errorInfo != null) {
showEmpty(errorInfo.icon, errorInfo.message)
} else {
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
}
} else {
showError(R.drawable.ic_info_accounts, getString(R.string.message_toast_no_account_selected))
}
}
}

View File

@ -48,9 +48,9 @@ object ParcelableMessageUtils {
val singleUrl = message.urlEntities?.singleOrNull()
if (singleUrl != null) {
if (singleUrl.expandedUrl.startsWith("https://twitter.com/i/stickers/image/")) {
return Pair(ParcelableMessage.Type.STICKER, StickerExtras(singleUrl.expandedUrl))
return Pair(ParcelableMessage.MessageType.STICKER, StickerExtras(singleUrl.expandedUrl))
}
}
return Pair(ParcelableMessage.Type.TEXT, null)
return Pair(ParcelableMessage.MessageType.TEXT, null)
}
}

View File

@ -13,6 +13,7 @@ import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.setFrom
import org.mariotaku.twidere.extension.model.timestamp
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationType
import org.mariotaku.twidere.model.util.AccountUtils.getAccountDetails
import org.mariotaku.twidere.model.util.ParcelableMessageUtils
import org.mariotaku.twidere.model.util.ParcelableUserUtils
@ -78,12 +79,12 @@ class GetMessagesTask(context: Context) : BaseAbstractTask<RefreshTaskParam, Uni
microBlog.getDirectMessages(paging).forEach { dm ->
val message = ParcelableMessageUtils.incomingMessage(accountKey, dm)
insertMessages.add(message)
conversations.addConversation(accountKey, message, dm.sender, dm.recipient)
conversations.addConversation(details, message, dm.sender, dm.recipient)
}
microBlog.getSentDirectMessages(paging).forEach { dm ->
val message = ParcelableMessageUtils.outgoingMessage(accountKey, dm)
insertMessages.add(message)
conversations.addConversation(accountKey, message, dm.sender, dm.recipient)
conversations.addConversation(details, message, dm.sender, dm.recipient)
}
return GetMessagesData(conversations.values, emptyList(), insertMessages)
}
@ -120,21 +121,23 @@ class GetMessagesTask(context: Context) : BaseAbstractTask<RefreshTaskParam, Uni
}
private fun MutableMap<String, ParcelableMessageConversation>.addConversation(
accountKey: UserKey,
details: AccountDetails,
message: ParcelableMessage,
vararg users: User
) {
val conversation = this[message.conversation_id] ?: run {
val obj = ParcelableMessageConversation()
obj.id = message.conversation_id
obj.conversation_type = ConversationType.ONE_TO_ONE
obj.setFrom(message, details)
this[message.conversation_id] = obj
obj.setFrom(message)
return@run obj
}
if (message.timestamp > conversation.timestamp) {
conversation.setFrom(message)
conversation.setFrom(message, details)
}
users.forEach { user ->
conversation.addParticipant(accountKey, user)
conversation.addParticipant(details.key, user)
}
}

View File

@ -54,6 +54,7 @@ import org.mariotaku.twidere.TwidereConstants.EXTRA_USER
import org.mariotaku.twidere.TwidereConstants.EXTRA_USER_LIST
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_ACCOUNT_KEY
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_CONVERSATION_ID
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_GROUP_ID
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_GROUP_NAME
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_LAT
@ -61,7 +62,6 @@ import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_LIST_ID
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_LIST_NAME
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_LNG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_QUERY
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_RECIPIENT_ID
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_SCREEN_NAME
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_STATUS_ID
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_TYPE
@ -279,16 +279,14 @@ object IntentUtils {
return builder.build()
}
fun openMessageConversation(context: Context,
accountKey: UserKey?,
recipientId: String?) {
fun openMessageConversation(context: Context, accountKey: UserKey?, conversationId: String?) {
val builder = Uri.Builder()
builder.scheme(SCHEME_TWIDERE)
builder.authority(AUTHORITY_DIRECT_MESSAGES_CONVERSATION)
if (accountKey != null) {
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString())
if (recipientId != null) {
builder.appendQueryParameter(QUERY_PARAM_RECIPIENT_ID, recipientId)
if (conversationId != null) {
builder.appendQueryParameter(QUERY_PARAM_CONVERSATION_ID, conversationId)
}
}
val intent = Intent(Intent.ACTION_VIEW, builder.build())

View File

@ -0,0 +1,77 @@
package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.synthetic.main.list_item_message_entry.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesConversationsAdapter
import org.mariotaku.twidere.extension.model.getConversationName
import org.mariotaku.twidere.extension.model.getSummaryText
import org.mariotaku.twidere.extension.model.timestamp
import org.mariotaku.twidere.extension.model.user
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationType
/**
* Created by mariotaku on 2017/2/9.
*/
class MessageConversationViewHolder(itemView: View, val adapter: MessagesConversationsAdapter) : RecyclerView.ViewHolder(itemView) {
private val content by lazy { itemView.content }
private val time by lazy { itemView.time }
private val name by lazy { itemView.name }
private val text by lazy { itemView.text }
private val profileImage by lazy { itemView.profileImage }
init {
setup()
}
fun display(conversation: ParcelableMessageConversation) {
if (adapter.drawAccountColors) {
content.drawEnd(conversation.account_color)
}else {
content.drawEnd()
}
val (name, secondaryName) = conversation.getConversationName(itemView.context)
this.time.time = conversation.timestamp
this.name.name = name
this.name.screenName = secondaryName
this.name.updateText(adapter.bidiFormatter)
this.text.text = conversation.getSummaryText(itemView.context)
if (conversation.conversation_type == ConversationType.ONE_TO_ONE) {
val user = conversation.user
if (user != null) {
adapter.mediaLoader.displayProfileImage(profileImage, user)
} else {
adapter.mediaLoader.cancelDisplayTask(profileImage)
// TODO display default profile image
}
} else {
adapter.mediaLoader.cancelDisplayTask(profileImage)
// TODO display default profile image
}
}
private fun setup() {
val textSize = adapter.textSize
name.setPrimaryTextSize(textSize * 1.1f)
name.setSecondaryTextSize(textSize)
text.textSize = textSize
time.textSize = textSize * 0.85f
itemView.setOnClickListener {
adapter.listener?.onConversationClick(layoutPosition)
}
profileImage.setOnClickListener {
adapter.listener?.onProfileImageClick(layoutPosition)
}
}
companion object {
const val layoutResource = R.layout.list_item_message_entry
}
}

View File

@ -660,5 +660,8 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
}
}
companion object {
const val layoutResource = R.layout.list_item_status
}
}

View File

@ -31,7 +31,7 @@
android:paddingLeft="@dimen/element_spacing_normal"
android:paddingRight="@dimen/element_spacing_normal"
app:ignorePadding="true"
tools:context=".adapter.MessageEntriesAdapter">
tools:context=".adapter.MessagesConversationsAdapter">
<org.mariotaku.twidere.view.ProfileImageView
android:id="@+id/profileImage"

View File

@ -671,6 +671,7 @@
<string name="message_permission_request_save_media">Twidere needs storage permission for saving media.</string>
<string name="message_permission_request_share_media">Twidere needs storage permission for sharing media to some apps.</string>
<string name="message_please_wait">Please wait.</string>
<string name="message_summary_type_sticker">[Sticker]</string>
<string name="message_sync_data_connect_hint">Connect Twidere with network storage to sync data</string>
<string
name="message_sync_data_synced_with_name">Twidere is now synced with <xliff:g