improved emoji support

This commit is contained in:
Mariotaku Lee 2017-05-13 14:19:23 +08:00
parent b2cf3bd2cb
commit c59fc51434
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
55 changed files with 540 additions and 605 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.bat text eol=crlf

View File

@ -1,80 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.preference;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import android.util.AttributeSet;
import java.util.List;
public abstract class ComponentPickerPreference extends ThemedListPreference {
protected final PackageManager packageManager;
public ComponentPickerPreference(final Context context) {
this(context, null);
}
public ComponentPickerPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
packageManager = context.getPackageManager();
init();
}
@Override
public CharSequence getSummary() {
if (isNoneValue(getValue())) return getNoneEntry();
return super.getSummary();
}
protected abstract String getIntentAction();
protected abstract String getNoneEntry();
private void init() {
final Intent queryIntent = new Intent(getIntentAction());
final List<ResolveInfo> infoList = resolve(queryIntent);
final int infoListSize = infoList.size();
final CharSequence[] entries = new CharSequence[infoListSize + 1], values = new CharSequence[infoListSize + 1];
entries[0] = getNoneEntry();
values[0] = "";
for (int i = 0; i < infoListSize; i++) {
final ResolveInfo info = infoList.get(i);
entries[i + 1] = info.loadLabel(packageManager);
values[i + 1] = getComponentName(info).flattenToString();
}
setEntries(entries);
setEntryValues(values);
}
protected abstract ComponentName getComponentName(ResolveInfo info);
protected abstract List<ResolveInfo> resolve(Intent queryIntent);
public static boolean isNoneValue(final String value) {
return TextUtils.isEmpty(value) || "none".equals(value);
}
}

View File

@ -1,49 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.preference;
import android.content.Context;
import android.util.AttributeSet;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.constant.IntentConstants;
/**
* Created by mariotaku on 15/12/22.
*/
public class EmojiSupportPreference extends ActivityPickerPreference {
public EmojiSupportPreference(Context context) {
super(context);
}
public EmojiSupportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected String getIntentAction() {
return IntentConstants.INTENT_ACTION_EMOJI_SUPPORT_ABOUT;
}
@Override
protected String getNoneEntry() {
return getContext().getString(R.string.system_default);
}
}

View File

@ -1,37 +0,0 @@
package org.mariotaku.twidere.preference;
import android.content.Context;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.util.AttributeSet;
import org.mariotaku.twidere.fragment.ThemedListPreferenceDialogFragmentCompat;
import org.mariotaku.twidere.preference.iface.IDialogPreference;
/**
* Created by mariotaku on 16/3/15.
*/
public class ThemedListPreference extends ListPreference implements IDialogPreference {
public ThemedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ThemedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ThemedListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ThemedListPreference(Context context) {
super(context);
}
@Override
public void displayDialog(PreferenceFragmentCompat fragment) {
ThemedListPreferenceDialogFragmentCompat df = ThemedListPreferenceDialogFragmentCompat.newInstance(getKey());
df.setTargetFragment(fragment, 0);
df.show(fragment.getFragmentManager(), getKey());
}
}

View File

@ -4,16 +4,22 @@ import android.graphics.Typeface
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
val TextView.empty: Boolean inline val TextView.empty: Boolean
get() = length() <= 0 get() = length() <= 0
var TextView.string: String? inline var TextView.string: String?
get() = text?.toString() get() = text?.toString()
set(value) { set(value) {
text = value text = value
} }
var TextView.charSequence: CharSequence? inline var TextView.spannable: CharSequence?
get() = text
set(value) {
setText(value, TextView.BufferType.SPANNABLE)
}
inline var TextView.charSequence: CharSequence?
get() = text get() = text
set(value) { set(value) {
text = value text = value
@ -25,9 +31,9 @@ fun TextView.applyFontFamily(lightFont: Boolean) {
} }
} }
fun TextView.hideIfEmpty() { fun TextView.hideIfEmpty(hideVisibility: Int = View.GONE) {
visibility = if (empty) { visibility = if (empty) {
View.GONE hideVisibility
} else { } else {
View.VISIBLE View.VISIBLE
} }

View File

@ -89,7 +89,7 @@ import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
import org.mariotaku.twidere.model.schedule.ScheduleInfo import org.mariotaku.twidere.model.schedule.ScheduleInfo
import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.preference.ServicePickerPreference import org.mariotaku.twidere.preference.ComponentPickerPreference
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
import org.mariotaku.twidere.service.LengthyOperationsService import org.mariotaku.twidere.service.LengthyOperationsService
import org.mariotaku.twidere.task.compose.AbsAddMediaTask import org.mariotaku.twidere.task.compose.AbsAddMediaTask
@ -250,7 +250,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
accountSelectorButton.setOnClickListener(this) accountSelectorButton.setOnClickListener(this)
replyLabel.setOnClickListener(this) replyLabel.setOnClickListener(this)
hintLabel.text = HtmlSpanBuilder.fromHtml(getString(R.string.hint_status_reply_to_user_removed)).apply { hintLabel.spannable = HtmlSpanBuilder.fromHtml(getString(R.string.hint_status_reply_to_user_removed)).apply {
val dialogSpan = getSpans(0, length, URLSpan::class.java).firstOrNull { val dialogSpan = getSpans(0, length, URLSpan::class.java).firstOrNull {
"#dialog" == it.url "#dialog" == it.url
} }
@ -357,8 +357,8 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
imageUploaderUsed = !ServicePickerPreference.isNoneValue(kPreferences[mediaUploaderKey]) imageUploaderUsed = !ComponentPickerPreference.isNoneValue(kPreferences[mediaUploaderKey])
statusShortenerUsed = !ServicePickerPreference.isNoneValue(kPreferences[statusShortenerKey]) statusShortenerUsed = !ComponentPickerPreference.isNoneValue(kPreferences[statusShortenerKey])
if (kPreferences[attachLocationKey]) { if (kPreferences[attachLocationKey]) {
if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_COARSE_LOCATION, if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_COARSE_LOCATION,
AndroidPermission.ACCESS_FINE_LOCATION)) { AndroidPermission.ACCESS_FINE_LOCATION)) {
@ -1162,7 +1162,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
return false return false
} }
val replyToName = userColorNameManager.getDisplayName(status, nameFirst) val replyToName = userColorNameManager.getDisplayName(status, nameFirst)
replyLabel.text = getString(R.string.label_quote_name_text, replyToName, status.text_unescaped) replyLabel.spannable = getString(R.string.label_quote_name_text, replyToName, status.text_unescaped)
replyLabel.visibility = View.VISIBLE replyLabel.visibility = View.VISIBLE
editText.hint = getString(R.string.label_quote_name, replyToName) editText.hint = getString(R.string.label_quote_name, replyToName)
return true return true
@ -1174,7 +1174,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
return false return false
} }
val replyToName = userColorNameManager.getDisplayName(status, nameFirst) val replyToName = userColorNameManager.getDisplayName(status, nameFirst)
replyLabel.text = getString(R.string.label_reply_name_text, replyToName, status.text_unescaped) replyLabel.spannable = getString(R.string.label_reply_name_text, replyToName, status.text_unescaped)
replyLabel.visibility = View.VISIBLE replyLabel.visibility = View.VISIBLE
editText.hint = getString(R.string.label_reply_name, replyToName) editText.hint = getString(R.string.label_reply_name, replyToName)
return true return true
@ -1362,7 +1362,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
if (location != null) { if (location != null) {
val attachPreciseLocation = kPreferences[attachPreciseLocationKey] val attachPreciseLocation = kPreferences[attachPreciseLocationKey]
if (attachPreciseLocation) { if (attachPreciseLocation) {
locationLabel.text = ParcelableLocationUtils.getHumanReadableString(location, 3) locationLabel.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3)
} else { } else {
if (locationLabel.tag == null || location != recentLocation) { if (locationLabel.tag == null || location != recentLocation) {
val task = DisplayPlaceNameTask() val task = DisplayPlaceNameTask()
@ -2086,12 +2086,12 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
val attachPreciseLocation = preferences[attachPreciseLocationKey] val attachPreciseLocation = preferences[attachPreciseLocationKey]
if (attachLocation) { if (attachLocation) {
if (attachPreciseLocation) { if (attachPreciseLocation) {
textView.text = ParcelableLocationUtils.getHumanReadableString(location, 3) textView.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3)
textView.tag = location textView.tag = location
} else { } else {
val tag = textView.tag val tag = textView.tag
if (tag is Address) { if (tag is Address) {
textView.text = tag.locality textView.spannable = tag.locality
} else if (tag is NoAddress) { } else if (tag is NoAddress) {
textView.setText(R.string.label_location_your_coarse_location) textView.setText(R.string.label_location_your_coarse_location)
} else { } else {
@ -2112,20 +2112,20 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
if (attachLocation) { if (attachLocation) {
if (attachPreciseLocation) { if (attachPreciseLocation) {
val location = params val location = params
textView.text = ParcelableLocationUtils.getHumanReadableString(location, 3) textView.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3)
textView.tag = location textView.tag = location
} else if (addresses == null || addresses.isEmpty()) { } else if (addresses == null || addresses.isEmpty()) {
val tag = textView.tag val tag = textView.tag
if (tag is Address) { if (tag is Address) {
textView.text = tag.locality textView.spannable = tag.locality
} else { } else {
textView.setText(R.string.label_location_your_coarse_location) textView.setText(R.string.label_location_your_coarse_location)
textView.tag = NoAddress() textView.tag = NoAddress()
} }
} else { } else {
val address = addresses[0] val address = addresses[0]
textView.spannable = address.locality
textView.tag = address textView.tag = address
textView.text = address.locality
} }
} else { } else {
textView.setText(R.string.no_location) textView.setText(R.string.no_location)

View File

@ -47,6 +47,7 @@ import jopt.csp.util.SortableIntList
import kotlinx.android.synthetic.main.activity_quick_search_bar.* import kotlinx.android.synthetic.main.activity_quick_search_bar.*
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.empty import org.mariotaku.ktextension.empty
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.BuildConfig import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_ACCOUNT_KEY import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_ACCOUNT_KEY
@ -392,23 +393,23 @@ class QuickSearchBarActivity : BaseActivity(), OnClickListener, LoaderCallbacks<
val holder = view.tag as SearchViewHolder val holder = view.tag as SearchViewHolder
val title = cursor.getString(indices.title) val title = cursor.getString(indices.title)
holder.edit_query.tag = title holder.edit_query.tag = title
holder.text1.text = title holder.text1.spannable = title
holder.icon.setImageResource(R.drawable.ic_action_history) holder.icon.setImageResource(R.drawable.ic_action_history)
} }
VIEW_TYPE_SAVED_SEARCH -> { VIEW_TYPE_SAVED_SEARCH -> {
val holder = view.tag as SearchViewHolder val holder = view.tag as SearchViewHolder
val title = cursor.getString(indices.title) val title = cursor.getString(indices.title)
holder.edit_query.tag = title holder.edit_query.tag = title
holder.text1.text = title holder.text1.spannable = title
holder.icon.setImageResource(R.drawable.ic_action_save) holder.icon.setImageResource(R.drawable.ic_action_save)
} }
VIEW_TYPE_USER_SUGGESTION_ITEM -> { VIEW_TYPE_USER_SUGGESTION_ITEM -> {
val holder = view.tag as UserViewHolder val holder = view.tag as UserViewHolder
val userKey = UserKey.valueOf(cursor.getString(indices.extra_id)) val userKey = UserKey.valueOf(cursor.getString(indices.extra_id))
holder.text1.text = userColorNameManager.getUserNickname(userKey, holder.text1.spannable = userColorNameManager.getUserNickname(userKey,
cursor.getString(indices.title)) cursor.getString(indices.title))
holder.text2.visibility = View.VISIBLE holder.text2.visibility = View.VISIBLE
holder.text2.text = "@${cursor.getString(indices.summary)}" holder.text2.spannable = "@${cursor.getString(indices.summary)}"
holder.icon.clearColorFilter() holder.icon.clearColorFilter()
requestManager.loadProfileImage(context, cursor.getString(indices.icon), requestManager.loadProfileImage(context, cursor.getString(indices.icon),
profileImageStyle, cornerRadius = holder.icon.cornerRadius, profileImageStyle, cornerRadius = holder.icon.cornerRadius,
@ -417,7 +418,7 @@ class QuickSearchBarActivity : BaseActivity(), OnClickListener, LoaderCallbacks<
} }
VIEW_TYPE_USER_SCREEN_NAME -> { VIEW_TYPE_USER_SCREEN_NAME -> {
val holder = view.tag as UserViewHolder val holder = view.tag as UserViewHolder
holder.text1.text = "@${cursor.getString(indices.title)}" holder.text1.spannable = "@${cursor.getString(indices.title)}"
holder.text2.visibility = View.GONE holder.text2.visibility = View.GONE
holder.icon.setColorFilter(holder.text1.currentTextColor, Mode.SRC_ATOP) holder.icon.setColorFilter(holder.text1.currentTextColor, Mode.SRC_ATOP)
//TODO cancel image load //TODO cancel image load

View File

@ -19,12 +19,12 @@
package org.mariotaku.twidere.adapter package org.mariotaku.twidere.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import kotlinx.android.synthetic.main.list_item_simple_user.view.* import kotlinx.android.synthetic.main.list_item_simple_user.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
@ -61,10 +61,9 @@ class AccountsSpinnerAdapter(
val icon = view.profileImage val icon = view.profileImage
if (!item.dummy) { if (!item.dummy) {
text1?.visibility = View.VISIBLE text1?.visibility = View.VISIBLE
text1?.text = item.user.name text1?.spannable = item.user.name
text2?.visibility = View.VISIBLE text2?.visibility = View.VISIBLE
@SuppressLint("SetTextI18n") text2?.spannable = "@${item.user.screen_name}"
text2?.text = "@${item.user.screen_name}"
if (icon != null) { if (icon != null) {
if (profileImageEnabled) { if (profileImageEnabled) {
icon.visibility = View.VISIBLE icon.visibility = View.VISIBLE
@ -76,7 +75,7 @@ class AccountsSpinnerAdapter(
} }
} else { } else {
text1?.visibility = View.VISIBLE text1?.visibility = View.VISIBLE
text1?.text = dummyItemText text1?.spannable = dummyItemText
text2?.visibility = View.GONE text2?.visibility = View.GONE
icon?.visibility = View.GONE icon?.visibility = View.GONE
} }

View File

@ -28,6 +28,7 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.constant.displayProfileImageKey import org.mariotaku.twidere.constant.displayProfileImageKey
@ -71,9 +72,9 @@ class ComposeAutoCompleteAdapter(context: Context, val requestManager: RequestMa
icon.style = profileImageStyle icon.style = profileImageStyle
if (Suggestions.AutoComplete.TYPE_USERS == cursor.getString(indices.type)) { if (Suggestions.AutoComplete.TYPE_USERS == cursor.getString(indices.type)) {
text1.text = userColorNameManager.getUserNickname(cursor.getString(indices.extra_id), text1.spannable = userColorNameManager.getUserNickname(cursor.getString(indices.extra_id),
cursor.getString(indices.title)) cursor.getString(indices.title))
text2.text = String.format("@%s", cursor.getString(indices.summary)) text2.spannable = String.format("@%s", cursor.getString(indices.summary))
if (displayProfileImage) { if (displayProfileImage) {
val profileImageUrl = cursor.getString(indices.icon) val profileImageUrl = cursor.getString(indices.icon)
requestManager.loadProfileImage(context, profileImageUrl, profileImageStyle).into(icon) requestManager.loadProfileImage(context, profileImageUrl, profileImageStyle).into(icon)
@ -83,7 +84,7 @@ class ComposeAutoCompleteAdapter(context: Context, val requestManager: RequestMa
icon.clearColorFilter() icon.clearColorFilter()
} else { } else {
text1.text = String.format("#%s", cursor.getString(indices.title)) text1.spannable = String.format("#%s", cursor.getString(indices.title))
text2.setText(R.string.hashtag) text2.setText(R.string.hashtag)
icon.setImageResource(R.drawable.ic_action_hashtag) icon.setImageResource(R.drawable.ic_action_hashtag)

View File

@ -30,6 +30,7 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.spannable
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.sqliteqb.library.Columns import org.mariotaku.sqliteqb.library.Columns
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
@ -38,7 +39,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.displayProfileImageKey import org.mariotaku.twidere.constant.displayProfileImageKey
import org.mariotaku.twidere.constant.profileImageStyleKey import org.mariotaku.twidere.constant.profileImageStyleKey
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.ParcelableLiteUser
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers
import org.mariotaku.twidere.util.UserColorNameManager import org.mariotaku.twidere.util.UserColorNameManager
@ -61,7 +62,7 @@ class UserAutoCompleteAdapter(
private val displayProfileImage: Boolean private val displayProfileImage: Boolean
private var profileImageStyle: Int private var profileImageStyle: Int
private var indices: ObjectCursor.CursorIndices<ParcelableUser>? = null private var indices: ObjectCursor.CursorIndices<ParcelableLiteUser>? = null
var accountKey: UserKey? = null var accountKey: UserKey? = null
@ -72,25 +73,22 @@ class UserAutoCompleteAdapter(
} }
override fun bindView(view: View, context: Context, cursor: Cursor) { override fun bindView(view: View, context: Context, cursor: Cursor) {
val indices = this.indices!! val user = indices!!.newObject(cursor)
val text1 = view.findViewById(android.R.id.text1) as TextView val text1 = view.findViewById(android.R.id.text1) as TextView
val text2 = view.findViewById(android.R.id.text2) as TextView val text2 = view.findViewById(android.R.id.text2) as TextView
val icon = view.findViewById(android.R.id.icon) as ProfileImageView val icon = view.findViewById(android.R.id.icon) as ProfileImageView
icon.style = profileImageStyle icon.style = profileImageStyle
text1.text = userColorNameManager.getUserNickname(cursor.getString(indices[CachedUsers.USER_KEY]), cursor.getString(indices[CachedUsers.NAME])) text1.spannable = userColorNameManager.getUserNickname(user.key, user.name)
@SuppressLint("SetTextI18n") text2.spannable = "@${user.screen_name}"
text2.text = "@${cursor.getString(indices[CachedUsers.SCREEN_NAME])}"
if (displayProfileImage) { if (displayProfileImage) {
val profileImageUrl = cursor.getString(indices[CachedUsers.PROFILE_IMAGE_URL]) requestManager.loadProfileImage(context, user, profileImageStyle).into(icon)
requestManager.loadProfileImage(context, profileImageUrl, profileImageStyle).into(icon)
} else { } else {
//TODO cancel image load //TODO cancel image load
} }
icon.visibility = if (displayProfileImage) View.VISIBLE else View.GONE icon.visibility = if (displayProfileImage) View.VISIBLE else View.GONE
super.bindView(view, context, cursor)
} }
fun closeCursor() { fun closeCursor() {
@ -100,8 +98,8 @@ class UserAutoCompleteAdapter(
} }
} }
override fun convertToString(cursor: Cursor?): CharSequence { override fun convertToString(cursor: Cursor): CharSequence {
return cursor!!.getString(indices!![CachedUsers.SCREEN_NAME]) return cursor.getString(indices!![CachedUsers.SCREEN_NAME])
} }
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? { override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
@ -127,7 +125,7 @@ class UserAutoCompleteAdapter(
} }
override fun swapCursor(cursor: Cursor?): Cursor? { override fun swapCursor(cursor: Cursor?): Cursor? {
indices = cursor?.let { ObjectCursor.indicesFrom(it, ParcelableUser::class.java) } indices = cursor?.let { ObjectCursor.indicesFrom(it, ParcelableLiteUser::class.java) }
return super.swapCursor(cursor) return super.swapCursor(cursor)
} }

View File

@ -0,0 +1,155 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.extension
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.Spanned
import android.widget.TextView
import org.mariotaku.commons.text.CodePointArray
import org.mariotaku.commons.text.get
import org.mariotaku.twidere.text.style.EmojiSpan
import org.mariotaku.twidere.text.util.EmojiEditableFactory
import org.mariotaku.twidere.text.util.EmojiSpannableFactory
import org.mariotaku.twidere.util.ExternalThemeManager
/**
* Created by mariotaku on 2017/5/13.
*/
fun TextView.setupEmojiFactory() {
if (isInEditMode) return
setSpannableFactory(EmojiSpannableFactory(this))
setEditableFactory(EmojiEditableFactory(this))
}
fun ExternalThemeManager.Emoji.applyTo(text: Spannable, textStart: Int = 0, textLength: Int = text.length) {
if (!isSupported) return
val array = CodePointArray(text)
var arrayIdx = array.length() - 1
while (arrayIdx >= 0) {
val codePoint = array[arrayIdx]
if (isEmoji(codePoint)) {
val arrayEnd = arrayIdx + 1
var arrayIdxOffset = 0
val textIdx = array.indexOfText(codePoint, arrayIdx)
var textIdxOffset = 0
var skippedIndex = 0
if (textIdx == -1 || textIdx < textStart) {
arrayIdx--
continue
}
val textEnd = textIdx + Character.charCount(codePoint)
if (arrayIdx > 0) {
val prevCodePoint = array[arrayIdx - 1]
when {
isRegionalIndicatorSymbol(codePoint) -> if (isRegionalIndicatorSymbol(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isModifier(codePoint) -> if (isEmoji(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isKeyCap(codePoint) -> if (isPhoneNumberSymbol(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isZeroWidthJoin(prevCodePoint) -> {
var notValidControlCount = 0
var charCount = 0
for (i in arrayIdx - 1 downTo 0) {
val cp = array.get(i)
charCount += Character.charCount(cp)
if (isZeroWidthJoin(cp) || isVariationSelector(cp)) {
// Ignore
notValidControlCount = 0
continue
}
notValidControlCount++
if (notValidControlCount > 1 || i == 0) {
arrayIdxOffset = i - arrayIdx + 1
textIdxOffset = -charCount + Character.charCount(cp)
skippedIndex = i - arrayIdx + 1
break
}
}
}
}
}
if (textEnd > textStart + textLength) {
arrayIdx--
continue
}
var spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java)
if (spans.isEmpty()) {
var drawable: Drawable? = getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd])
if (drawable == null) {
// Not emoji combination, just use fallback
textIdxOffset = 0
arrayIdxOffset = 0
skippedIndex = 0
spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java)
if (spans.isEmpty()) {
drawable = getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd])
}
}
if (drawable != null) {
text.setSpan(EmojiSpan(drawable), textIdx + textIdxOffset, textEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
arrayIdx += skippedIndex
}
arrayIdx--
}
}
private fun isVariationSelector(codePoint: Int): Boolean {
return codePoint == 0xfe0f
}
private fun isZeroWidthJoin(codePoint: Int): Boolean {
return codePoint == 0x200d
}
private fun isPhoneNumberSymbol(codePoint: Int): Boolean {
return codePoint == 0x0023 || codePoint == 0x002a || codePoint in 0x0030..0x0039
}
private fun isModifier(codePoint: Int): Boolean {
return codePoint in 0x1f3fb..0x1f3ff
}
private fun isEmoji(codePoint: Int): Boolean {
return !Character.isLetterOrDigit(codePoint)
}
private fun isRegionalIndicatorSymbol(codePoint: Int): Boolean {
return codePoint in 0x1f1e6..0x1f1ff
}
private fun isKeyCap(codePoint: Int): Boolean {
return codePoint == 0x20e3
}

View File

@ -23,6 +23,8 @@ import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.support.annotation.UiThread import android.support.annotation.UiThread
import android.view.View import android.view.View
import android.widget.TextView
import org.mariotaku.ktextension.empty
private val tempLocation = IntArray(2) private val tempLocation = IntArray(2)
private val tempRect = Rect() private val tempRect = Rect()
@ -70,6 +72,14 @@ fun View.removeSystemUiVisibility(systemUiVisibility: Int) {
this.systemUiVisibility = this.systemUiVisibility and systemUiVisibility.inv() this.systemUiVisibility = this.systemUiVisibility and systemUiVisibility.inv()
} }
fun View.hideIfEmpty(dependency: TextView, hideVisibility: Int = View.GONE) {
visibility = if (dependency.empty) {
hideVisibility
} else {
View.VISIBLE
}
}
private fun offsetToRoot(view: View, rect: Rect) { private fun offsetToRoot(view: View, rect: Rect) {
var parent = view.parent as? View var parent = view.parent as? View
while (parent != null) { while (parent != null) {

View File

@ -25,6 +25,7 @@ import android.os.Bundle
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.TextView import android.widget.TextView
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_USER import org.mariotaku.twidere.constant.IntentConstants.EXTRA_USER
import org.mariotaku.twidere.extension.applyTheme import org.mariotaku.twidere.extension.applyTheme
@ -61,7 +62,7 @@ abstract class AbsUserMuteBlockDialogFragment : BaseDialogFragment(), DialogInte
MessageDialogFragment.show(childFragmentManager, title = getString(R.string.filter_everywhere), MessageDialogFragment.show(childFragmentManager, title = getString(R.string.filter_everywhere),
message = getString(R.string.filter_everywhere_description), tag = "filter_everywhere_help") message = getString(R.string.filter_everywhere_description), tag = "filter_everywhere_help")
} }
confirmMessageView.text = getMessage(user) confirmMessageView.spannable = getMessage(user)
} }
return dialog return dialog
} }

View File

@ -55,10 +55,7 @@ import kotlinx.android.synthetic.main.header_drawer_account_selector.view.*
import org.mariotaku.chameleon.Chameleon import org.mariotaku.chameleon.Chameleon
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.kpreferences.set import org.mariotaku.kpreferences.set
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe import org.mariotaku.ktextension.*
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.setItemAvailability
import org.mariotaku.ktextension.setMenuItemIcon
import org.mariotaku.twidere.Constants.EXTRA_FEATURES_NOTICE_VERSION import org.mariotaku.twidere.Constants.EXTRA_FEATURES_NOTICE_VERSION
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.TwidereConstants.*
@ -546,8 +543,8 @@ class AccountsDashboardFragment : BaseFragment(), LoaderCallbacks<AccountsInfo>,
private fun displayCurrentAccount(profileImageSnapshot: Drawable?) { private fun displayCurrentAccount(profileImageSnapshot: Drawable?) {
if (context == null || isDetached || (activity?.isFinishing ?: true)) return if (context == null || isDetached || (activity?.isFinishing ?: true)) return
val account = accountsAdapter.selectedAccount ?: return val account = accountsAdapter.selectedAccount ?: return
accountProfileNameView.text = account.user.name accountProfileNameView.spannable = account.user.name
accountProfileScreenNameView.text = "@${account.user.screen_name}" accountProfileScreenNameView.spannable = "@${account.user.screen_name}"
Glide.with(this).loadProfileImage(context, account, preferences[profileImageStyleKey], Glide.with(this).loadProfileImage(context, account, preferences[profileImageStyleKey],
accountProfileImageView.cornerRadius, accountProfileImageView.cornerRadiusRatio, accountProfileImageView.cornerRadius, accountProfileImageView.cornerRadiusRatio,
ProfileImageSize.REASONABLY_SMALL).placeholder(profileImageSnapshot).into(accountProfileImageView) ProfileImageSize.REASONABLY_SMALL).placeholder(profileImageSnapshot).into(accountProfileImageView)

View File

@ -81,6 +81,8 @@ open class BaseFragment : Fragment(), IBaseFragment<BaseFragment> {
lateinit var dns: Dns lateinit var dns: Dns
@Inject @Inject
lateinit var syncPreferences: SyncPreferences lateinit var syncPreferences: SyncPreferences
@Inject
lateinit var externalThemeManager: ExternalThemeManager
protected val statusScheduleProvider: StatusScheduleProvider? protected val statusScheduleProvider: StatusScheduleProvider?
get() = statusScheduleProviderFactory.newInstance(context) get() = statusScheduleProviderFactory.newInstance(context)

View File

@ -34,7 +34,6 @@ import android.support.v4.app.LoaderManager.LoaderCallbacks
import android.support.v4.content.CursorLoader import android.support.v4.content.CursorLoader
import android.support.v4.content.Loader import android.support.v4.content.Loader
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.text.TextUtils
import android.util.SparseArray import android.util.SparseArray
import android.view.* import android.view.*
import android.widget.* import android.widget.*
@ -48,6 +47,7 @@ import org.mariotaku.chameleon.Chameleon
import org.mariotaku.ktextension.Bundle import org.mariotaku.ktextension.Bundle
import org.mariotaku.ktextension.contains import org.mariotaku.ktextension.contains
import org.mariotaku.ktextension.set import org.mariotaku.ktextension.set
import org.mariotaku.ktextension.spannable
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.sqliteqb.library.Columns.Column import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
@ -439,7 +439,7 @@ class CustomTabsFragment : BaseFragment(), LoaderCallbacks<Cursor?>, MultiChoice
view.findViewById(android.R.id.text2).visibility = View.GONE view.findViewById(android.R.id.text2).visibility = View.GONE
val text1 = view.findViewById(android.R.id.text1) as TextView val text1 = view.findViewById(android.R.id.text1) as TextView
val item = getItem(position) val item = getItem(position)
text1.text = item.name text1.spannable = item.name
bindIconView(item, view) bindIconView(item, view)
return view return view
} }
@ -484,12 +484,12 @@ class CustomTabsFragment : BaseFragment(), LoaderCallbacks<Cursor?>, MultiChoice
val iconKey = tempTab.icon val iconKey = tempTab.icon
if (type != null && CustomTabUtils.isTabTypeValid(type)) { if (type != null && CustomTabUtils.isTabTypeValid(type)) {
val typeName = CustomTabUtils.getTabTypeName(context, type) val typeName = CustomTabUtils.getTabTypeName(context, type)
holder.text1.text = if (TextUtils.isEmpty(name)) typeName else name holder.text1.spannable = name?.takeIf(String::isNotEmpty) ?: typeName
holder.text1.paintFlags = holder.text1.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() holder.text1.paintFlags = holder.text1.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
holder.text2.visibility = View.VISIBLE holder.text2.visibility = View.VISIBLE
holder.text2.text = typeName holder.text2.text = typeName
} else { } else {
holder.text1.text = name holder.text1.spannable = name
holder.text1.paintFlags = holder.text1.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG holder.text1.paintFlags = holder.text1.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
holder.text2.setText(R.string.invalid_tab) holder.text2.setText(R.string.invalid_tab)
} }

View File

@ -62,10 +62,7 @@ import kotlinx.android.synthetic.main.header_status.view.*
import kotlinx.android.synthetic.main.layout_content_fragment_common.* import kotlinx.android.synthetic.main.layout_content_fragment_common.*
import org.mariotaku.abstask.library.TaskStarter import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.applyFontFamily import org.mariotaku.ktextension.*
import org.mariotaku.ktextension.contains
import org.mariotaku.ktextension.findPositionByItemId
import org.mariotaku.ktextension.hideIfEmpty
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
@ -424,7 +421,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE
setState(STATE_ERROR) setState(STATE_ERROR)
val errorInfo = StatusCodeMessageUtils.getErrorInfo(context, data.exception!!) val errorInfo = StatusCodeMessageUtils.getErrorInfo(context, data.exception!!)
errorText.text = errorInfo.message errorText.spannable = errorInfo.message
errorIcon.setImageResource(errorInfo.icon) errorIcon.setImageResource(errorInfo.icon)
} }
activity.supportInvalidateOptionsMenu() activity.supportInvalidateOptionsMenu()
@ -742,10 +739,10 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
if (status.retweet_id != null) { if (status.retweet_id != null) {
val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!, val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!,
status.retweeted_by_user_name!!, status.retweeted_by_user_acct!!, nameFirst) status.retweeted_by_user_name!!, status.retweeted_by_user_acct!!, nameFirst)
retweetedByView.text = context.getString(R.string.name_retweeted, retweetedBy) retweetedByView.spannable = context.getString(R.string.name_retweeted, retweetedBy)
retweetedByView.visibility = View.VISIBLE retweetedByView.visibility = View.VISIBLE
} else { } else {
retweetedByView.text = null retweetedByView.spannable = null
retweetedByView.visibility = View.GONE retweetedByView.visibility = View.GONE
} }
@ -776,16 +773,11 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(), linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(),
status.is_possibly_sensitive, skipLinksInText) status.is_possibly_sensitive, skipLinksInText)
if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) {
itemView.quotedText.text = quotedText.subSequence(0, quotedDisplayEnd) itemView.quotedText.spannable = quotedText.subSequence(0, quotedDisplayEnd)
} else { } else {
itemView.quotedText.text = quotedText itemView.quotedText.spannable = quotedText
}
if (itemView.quotedText.length() == 0) {
// No text
itemView.quotedText.visibility = View.GONE
} else {
itemView.quotedText.visibility = View.VISIBLE
} }
itemView.quotedText.hideIfEmpty()
val quotedUserColor = colorNameManager.getUserColor(status.quoted_user_key!!) val quotedUserColor = colorNameManager.getUserColor(status.quoted_user_key!!)
if (quotedUserColor != 0) { if (quotedUserColor != 0) {
@ -821,7 +813,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
string.setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, string.setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context,
android.R.attr.textColorTertiary, textView.currentTextColor)), 0, android.R.attr.textColorTertiary, textView.currentTextColor)), 0,
string.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) string.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
itemView.quotedText.text = string itemView.quotedText.spannable = string
itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, itemView.quotedView.drawStart(ThemeUtils.getColorFromAttribute(context,
R.attr.quoteIndicatorBackgroundColor)) R.attr.quoteIndicatorBackgroundColor))
@ -864,7 +856,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
val timeString = Utils.formatToLongTimeString(context, timestamp)?.takeIf(String::isNotEmpty) val timeString = Utils.formatToLongTimeString(context, timestamp)?.takeIf(String::isNotEmpty)
val source = status.source?.takeIf(String::isNotEmpty) val source = status.source?.takeIf(String::isNotEmpty)
itemView.timeSource.text = when { itemView.timeSource.spannable = when {
timeString != null && source != null -> { timeString != null && source != null -> {
HtmlSpanBuilder.fromHtml(context.getString(R.string.status_format_time_source, HtmlSpanBuilder.fromHtml(context.getString(R.string.status_format_time_source,
timeString, source)) timeString, source))
@ -882,13 +874,13 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
status.is_possibly_sensitive, skipLinksInText) status.is_possibly_sensitive, skipLinksInText)
} }
summaryView.text = status.extras?.summary_text summaryView.spannable = status.extras?.summary_text
summaryView.hideIfEmpty() summaryView.hideIfEmpty()
if (displayEnd != -1 && displayEnd <= text.length) { if (displayEnd != -1 && displayEnd <= text.length) {
textView.text = text.subSequence(0, displayEnd) textView.spannable = text.subSequence(0, displayEnd)
} else { } else {
textView.text = text textView.spannable = text
} }
textView.hideIfEmpty() textView.hideIfEmpty()
@ -897,7 +889,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
if (!TextUtils.isEmpty(placeFullName)) { if (!TextUtils.isEmpty(placeFullName)) {
locationView.visibility = View.VISIBLE locationView.visibility = View.VISIBLE
locationView.text = placeFullName locationView.spannable = placeFullName
locationView.isClickable = ParcelableLocationUtils.isValidLocation(location) locationView.isClickable = ParcelableLocationUtils.isValidLocation(location)
} else if (ParcelableLocationUtils.isValidLocation(location)) { } else if (ParcelableLocationUtils.isValidLocation(location)) {
locationView.visibility = View.VISIBLE locationView.visibility = View.VISIBLE
@ -905,7 +897,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
locationView.isClickable = true locationView.isClickable = true
} else { } else {
locationView.visibility = View.GONE locationView.visibility = View.GONE
locationView.text = null locationView.spannable = null
} }
val interactUsersAdapter = itemView.countsUsers.adapter as CountsUsersAdapter val interactUsersAdapter = itemView.countsUsers.adapter as CountsUsersAdapter

View File

@ -69,6 +69,7 @@ import android.view.*
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.squareup.otto.Subscribe import com.squareup.otto.Subscribe
@ -429,11 +430,10 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
this.user = user this.user = user
profileImage.setBorderColor(if (user.color != 0) user.color else Color.WHITE) profileImage.setBorderColor(if (user.color != 0) user.color else Color.WHITE)
profileNameContainer.drawEnd(user.account_color) profileNameContainer.drawEnd(user.account_color)
profileNameContainer.name.text = bidiFormatter.unicodeWrap(if (user.nickname.isNullOrEmpty()) { profileNameContainer.name.setText(bidiFormatter.unicodeWrap(when {
user.name user.nickname.isNullOrEmpty() -> user.name
} else { else -> getString(R.string.name_with_nickname, user.name, user.nickname)
getString(R.string.name_with_nickname, user.name, user.nickname) }), TextView.BufferType.SPANNABLE)
})
val typeIconRes = Utils.getUserTypeIconRes(user.is_verified, user.is_protected) val typeIconRes = Utils.getUserTypeIconRes(user.is_verified, user.is_protected)
if (typeIconRes != 0) { if (typeIconRes != 0) {
profileType.setImageResource(typeIconRes) profileType.setImageResource(typeIconRes)
@ -443,7 +443,7 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
profileType.visibility = View.GONE profileType.visibility = View.GONE
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
profileNameContainer.screenName.text = "@${user.acct}" profileNameContainer.screenName.spannable = "@${user.acct}"
val linkHighlightOption = preferences[linkHighlightOptionKey] val linkHighlightOption = preferences[linkHighlightOptionKey]
val linkify = TwidereLinkify(this, linkHighlightOption) val linkify = TwidereLinkify(this, linkHighlightOption)
if (user.description_unescaped != null) { if (user.description_unescaped != null) {
@ -451,22 +451,22 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
user.description_spans?.applyTo(this) user.description_spans?.applyTo(this)
linkify.applyAllLinks(this, user.account_key, false, false) linkify.applyAllLinks(this, user.account_key, false, false)
} }
descriptionContainer.description.text = text descriptionContainer.description.spannable = text
} else { } else {
descriptionContainer.description.text = user.description_plain descriptionContainer.description.spannable = user.description_plain
Linkify.addLinks(descriptionContainer.description, Linkify.WEB_URLS) Linkify.addLinks(descriptionContainer.description, Linkify.WEB_URLS)
} }
descriptionContainer.visibility = if (descriptionContainer.description.empty) View.GONE else View.VISIBLE descriptionContainer.hideIfEmpty(descriptionContainer.description)
locationContainer.location.text = user.location locationContainer.location.spannable = user.location
locationContainer.visibility = if (locationContainer.location.empty) View.GONE else View.VISIBLE locationContainer.visibility = if (locationContainer.location.empty) View.GONE else View.VISIBLE
urlContainer.url.text = user.urlPreferred?.let { urlContainer.url.spannable = user.urlPreferred?.let {
val ssb = SpannableStringBuilder(it) val ssb = SpannableStringBuilder(it)
ssb.setSpan(TwidereURLSpan(it, highlightStyle = linkHighlightOption), 0, ssb.length, ssb.setSpan(TwidereURLSpan(it, highlightStyle = linkHighlightOption), 0, ssb.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return@let ssb return@let ssb
} }
urlContainer.visibility = if (urlContainer.url.empty) View.GONE else View.VISIBLE urlContainer.hideIfEmpty(urlContainer.url)
if (user.created_at >= 0) { if (user.created_at >= 0) {
val createdAt = Utils.formatToLongTimeString(activity, user.created_at) val createdAt = Utils.formatToLongTimeString(activity, user.created_at)
val daysSinceCreation = (System.currentTimeMillis() - user.created_at) / 1000 / 60 / 60 / 24.toFloat() val daysSinceCreation = (System.currentTimeMillis() - user.created_at) / 1000 / 60 / 60 / 24.toFloat()
@ -509,8 +509,10 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
if (relationship == null) { if (relationship == null) {
getFriendship() getFriendship()
} }
activity.title = UserColorNameManager.decideDisplayName(user.nickname, user.name, activity.title = SpannableStringBuilder.valueOf(UserColorNameManager.decideDisplayName(user.nickname, user.name,
user.screen_name, nameFirst) user.screen_name, nameFirst)).also {
externalThemeManager.emoji?.applyTo(it)
}
val userCreationDay = condition@ if (user.created_at >= 0) { val userCreationDay = condition@ if (user.created_at >= 0) {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()

View File

@ -20,6 +20,7 @@ import android.widget.TextView
import kotlinx.android.synthetic.main.fragment_content_listview.* import kotlinx.android.synthetic.main.fragment_content_listview.*
import org.mariotaku.kpreferences.KPreferences import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.ktextension.setItemAvailability import org.mariotaku.ktextension.setItemAvailability
import org.mariotaku.ktextension.spannable
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.TwidereConstants.*
@ -177,7 +178,7 @@ class FilteredUsersFragment : BaseFiltersFragment() {
val screenName = cursor.getString(indices[Filters.Users.SCREEN_NAME]) val screenName = cursor.getString(indices[Filters.Users.SCREEN_NAME])
val displayName = userColorNameManager.getDisplayName(userKey, name, screenName, val displayName = userColorNameManager.getDisplayName(userKey, name, screenName,
nameFirst) nameFirst)
text1.text = displayName text1.spannable = displayName
val ssb = SpannableStringBuilder(displayName) val ssb = SpannableStringBuilder(displayName)
if (cursor.getLong(indices[Filters.Users.SOURCE]) >= 0) { if (cursor.getLong(indices[Filters.Users.SOURCE]) >= 0) {
@ -188,8 +189,8 @@ class FilteredUsersFragment : BaseFiltersFragment() {
drawable.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_ATOP) drawable.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_ATOP)
ssb.setSpan(EmojiSpan(drawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) ssb.setSpan(EmojiSpan(drawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
text1.text = ssb text1.spannable = ssb
text2.text = userKey.host text2.spannable = userKey.host
} }
override fun swapCursor(c: Cursor?): Cursor? { override fun swapCursor(c: Cursor?): Cursor? {

View File

@ -57,6 +57,7 @@ import org.mariotaku.chameleon.ChameleonUtils
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.mapToArray import org.mariotaku.ktextension.mapToArray
import org.mariotaku.ktextension.setItemAvailability import org.mariotaku.ktextension.setItemAvailability
import org.mariotaku.ktextension.spannable
import org.mariotaku.ktextension.useCursor import org.mariotaku.ktextension.useCursor
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
@ -260,14 +261,14 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
val profileImageStyle = preferences[profileImageStyleKey] val profileImageStyle = preferences[profileImageStyleKey]
requestManager.loadProfileImage(context, data, profileImageStyle).into(conversationAvatar) requestManager.loadProfileImage(context, data, profileImageStyle).into(conversationAvatar)
requestManager.loadProfileImage(context, data, profileImageStyle, size = ProfileImageSize.REASONABLY_SMALL).into(appBarIcon) requestManager.loadProfileImage(context, data, profileImageStyle, size = ProfileImageSize.REASONABLY_SMALL).into(appBarIcon)
appBarTitle.text = name appBarTitle.spannable = name
conversationTitle.text = name conversationTitle.spannable = name
if (summary != null) { if (summary != null) {
appBarSubtitle.visibility = View.VISIBLE appBarSubtitle.visibility = View.VISIBLE
conversationSubtitle.visibility = View.VISIBLE conversationSubtitle.visibility = View.VISIBLE
appBarSubtitle.text = summary appBarSubtitle.spannable = summary
conversationSubtitle.text = summary conversationSubtitle.spannable = summary
} else { } else {
appBarSubtitle.visibility = View.GONE appBarSubtitle.visibility = View.GONE
conversationSubtitle.visibility = View.GONE conversationSubtitle.visibility = View.GONE

View File

@ -493,10 +493,10 @@ class MessagesConversationFragment : AbsContentListRecyclerViewFragment<Messages
sendMessage.isEnabled = !readOnly sendMessage.isEnabled = !readOnly
editText.isEnabled = !readOnly editText.isEnabled = !readOnly
conversationTitle.text = title conversationTitle.spannable = title
if (subtitle != null) { if (subtitle != null) {
conversationSubtitle.visibility = View.VISIBLE conversationSubtitle.visibility = View.VISIBLE
conversationSubtitle.text = subtitle conversationSubtitle.spannable = subtitle
} else { } else {
conversationSubtitle.visibility = View.GONE conversationSubtitle.visibility = View.GONE
} }

View File

@ -32,6 +32,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.TextView import android.widget.TextView
import org.mariotaku.chameleon.Chameleon import org.mariotaku.chameleon.Chameleon
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.Constants.* import org.mariotaku.twidere.Constants.*
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.activity.ComposeActivity import org.mariotaku.twidere.activity.ComposeActivity
@ -105,7 +106,7 @@ class SearchFragment : AbsToolbarTabPagesFragment(), RefreshScrollTopInterface,
val customView = actionBar.customView val customView = actionBar.customView
val editQuery = customView.findViewById(R.id.editQuery) as TextView val editQuery = customView.findViewById(R.id.editQuery) as TextView
editQuery.setTextColor(ThemeUtils.getColorDependent(theme.colorToolbar)) editQuery.setTextColor(ThemeUtils.getColorDependent(theme.colorToolbar))
editQuery.text = query editQuery.spannable = query
customView.setOnClickListener { customView.setOnClickListener {
val searchIntent = Intent(context, QuickSearchBarActivity::class.java).apply { val searchIntent = Intent(context, QuickSearchBarActivity::class.java).apply {
putExtra(EXTRA_QUERY, query) putExtra(EXTRA_QUERY, query)

View File

@ -17,33 +17,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.mariotaku.twidere.preference; package org.mariotaku.twidere.preference
import android.content.ComponentName; import android.content.ComponentName
import android.content.Context; import android.content.Context
import android.content.Intent; import android.content.Intent
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo
import android.util.AttributeSet; import android.util.AttributeSet
import java.util.List; abstract class ActivityPickerPreference(context: Context, attrs: AttributeSet? = null) :
ComponentPickerPreference(context, attrs) {
public abstract class ActivityPickerPreference extends ComponentPickerPreference { override fun getComponentName(info: ResolveInfo): ComponentName {
return ComponentName(info.activityInfo.packageName, info.activityInfo.name)
public ActivityPickerPreference(final Context context) {
this(context, null);
} }
public ActivityPickerPreference(final Context context, final AttributeSet attrs) { override fun resolve(queryIntent: Intent): List<ResolveInfo> {
super(context, attrs); return packageManager.queryIntentActivities(queryIntent, 0)
}
@Override
protected final ComponentName getComponentName(ResolveInfo info) {
return new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
}
@Override
protected List<ResolveInfo> resolve(Intent queryIntent) {
return packageManager.queryIntentActivities(queryIntent, 0);
} }
} }

View File

@ -0,0 +1,74 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.preference
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.util.AttributeSet
abstract class ComponentPickerPreference(context: Context, attrs: AttributeSet? = null) : ThemedListPreference(context, attrs) {
protected val packageManager: PackageManager = context.packageManager
init {
setup()
}
override fun getSummary(): CharSequence {
if (isNoneValue(value)) return noneEntry
return entry
}
protected abstract val intentAction: String
protected abstract val noneEntry: String
protected abstract fun getComponentName(info: ResolveInfo): ComponentName
protected abstract fun resolve(queryIntent: Intent): List<ResolveInfo>
private fun setup() {
val queryIntent = Intent(intentAction)
val infoList = resolve(queryIntent)
val infoListSize = infoList.size
val entries = arrayOfNulls<CharSequence>(infoListSize + 1)
val values = arrayOfNulls<CharSequence>(infoListSize + 1)
entries[0] = noneEntry
values[0] = ""
for (i in 0..infoListSize - 1) {
val info = infoList[i]
entries[i + 1] = info.loadLabel(packageManager)
values[i + 1] = getComponentName(info).flattenToString()
}
setEntries(entries)
entryValues = values
}
companion object {
fun isNoneValue(value: String?): Boolean {
return value.isNullOrEmpty() || "none" == value
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.preference
import android.content.Context
import android.util.AttributeSet
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.IntentConstants
/**
* Created by mariotaku on 15/12/22.
*/
class EmojiSupportPreference(context: Context, attrs: AttributeSet? = null) :
ActivityPickerPreference(context, attrs) {
override val intentAction: String
get() = IntentConstants.INTENT_ACTION_EMOJI_SUPPORT_ABOUT
override val noneEntry: String
get() = context.getString(R.string.system_default)
}

View File

@ -17,32 +17,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.mariotaku.twidere.preference; package org.mariotaku.twidere.preference
import android.content.Context; import android.content.Context
import android.util.AttributeSet; import android.util.AttributeSet
import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.R
import org.mariotaku.twidere.R;
public class StatusShortenerPreference extends ServicePickerPreference implements Constants { import org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_EXTENSION_UPLOAD_MEDIA
public StatusShortenerPreference(final Context context) { class MediaUploaderPreference(context: Context, attrs: AttributeSet? = null) :
this(context, null); ServicePickerPreference(context, attrs) {
}
public StatusShortenerPreference(final Context context, final AttributeSet attrs) { override val intentAction: String
super(context, attrs); get() = INTENT_ACTION_EXTENSION_UPLOAD_MEDIA
}
@Override override val noneEntry: String
protected String getIntentAction() { get() = context.getString(R.string.media_uploader_default)
return INTENT_ACTION_EXTENSION_SHORTEN_STATUS;
}
@Override
protected String getNoneEntry() {
return getContext().getString(R.string.status_shortener_default);
}
} }

View File

@ -17,33 +17,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.mariotaku.twidere.preference; package org.mariotaku.twidere.preference
import android.content.ComponentName; import android.content.ComponentName
import android.content.Context; import android.content.Context
import android.content.Intent; import android.content.Intent
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo
import android.util.AttributeSet; import android.util.AttributeSet
import java.util.List; abstract class ServicePickerPreference(context: Context, attrs: AttributeSet?) :
ComponentPickerPreference(context, attrs) {
public abstract class ServicePickerPreference extends ComponentPickerPreference { override fun getComponentName(info: ResolveInfo): ComponentName {
return ComponentName(info.serviceInfo.packageName, info.serviceInfo.name)
public ServicePickerPreference(final Context context) {
this(context, null);
} }
public ServicePickerPreference(final Context context, final AttributeSet attrs) { override fun resolve(queryIntent: Intent): List<ResolveInfo> {
super(context, attrs); return packageManager.queryIntentServices(queryIntent, 0)
}
@Override
protected final ComponentName getComponentName(ResolveInfo info) {
return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name);
}
@Override
protected List<ResolveInfo> resolve(Intent queryIntent) {
return packageManager.queryIntentServices(queryIntent, 0);
} }
} }

View File

@ -17,33 +17,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.mariotaku.twidere.preference; package org.mariotaku.twidere.preference
import android.content.Context; import android.content.Context
import android.util.AttributeSet; import android.util.AttributeSet
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_EXTENSION_SHORTEN_STATUS
import org.mariotaku.twidere.R; class StatusShortenerPreference(context: Context, attrs: AttributeSet?) :
ServicePickerPreference(context, attrs), Constants {
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_EXTENSION_UPLOAD_MEDIA; override val intentAction: String
get() = INTENT_ACTION_EXTENSION_SHORTEN_STATUS
public class MediaUploaderPreference extends ServicePickerPreference { override val noneEntry: String
get() = context.getString(R.string.status_shortener_default)
public MediaUploaderPreference(final Context context) {
super(context);
}
public MediaUploaderPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
protected String getIntentAction() {
return INTENT_ACTION_EXTENSION_UPLOAD_MEDIA;
}
@Override
protected String getNoneEntry() {
return getContext().getString(R.string.media_uploader_default);
}
} }

View File

@ -0,0 +1,22 @@
package org.mariotaku.twidere.preference
import android.content.Context
import android.support.v7.preference.ListPreference
import android.support.v7.preference.PreferenceFragmentCompat
import android.util.AttributeSet
import org.mariotaku.twidere.fragment.ThemedListPreferenceDialogFragmentCompat
import org.mariotaku.twidere.preference.iface.IDialogPreference
/**
* Created by mariotaku on 16/3/15.
*/
open class ThemedListPreference(context: Context, attrs: AttributeSet? = null) :
ListPreference(context, attrs), IDialogPreference {
override fun displayDialog(fragment: PreferenceFragmentCompat) {
val df = ThemedListPreferenceDialogFragmentCompat.newInstance(key)
df.setTargetFragment(fragment, 0)
df.show(fragment.fragmentManager, key)
}
}

View File

@ -51,7 +51,7 @@ import org.mariotaku.twidere.model.account.AccountExtras
import org.mariotaku.twidere.model.analyzer.UpdateStatus import org.mariotaku.twidere.model.analyzer.UpdateStatus
import org.mariotaku.twidere.model.schedule.ScheduleInfo import org.mariotaku.twidere.model.schedule.ScheduleInfo
import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.preference.ServicePickerPreference import org.mariotaku.twidere.preference.ComponentPickerPreference
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
import org.mariotaku.twidere.task.BaseAbstractTask import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.*
@ -496,7 +496,7 @@ class UpdateStatusTask(
@Throws(UploaderNotFoundException::class, UploadException::class, ShortenerNotFoundException::class, ShortenException::class) @Throws(UploaderNotFoundException::class, UploadException::class, ShortenerNotFoundException::class, ShortenException::class)
private fun getStatusShortener(app: TwidereApplication): StatusShortenerInterface? { private fun getStatusShortener(app: TwidereApplication): StatusShortenerInterface? {
val shortenerComponent = preferences.getString(KEY_STATUS_SHORTENER, null) val shortenerComponent = preferences.getString(KEY_STATUS_SHORTENER, null)
if (ServicePickerPreference.isNoneValue(shortenerComponent)) return null if (ComponentPickerPreference.isNoneValue(shortenerComponent)) return null
val shortener = StatusShortenerInterface.getInstance(app, shortenerComponent) ?: throw ShortenerNotFoundException() val shortener = StatusShortenerInterface.getInstance(app, shortenerComponent) ?: throw ShortenerNotFoundException()
try { try {
@ -518,7 +518,7 @@ class UpdateStatusTask(
@Throws(UploaderNotFoundException::class, UploadException::class) @Throws(UploaderNotFoundException::class, UploadException::class)
private fun getMediaUploader(app: TwidereApplication): MediaUploaderInterface? { private fun getMediaUploader(app: TwidereApplication): MediaUploaderInterface? {
val uploaderComponent = preferences.getString(KEY_MEDIA_UPLOADER, null) val uploaderComponent = preferences.getString(KEY_MEDIA_UPLOADER, null)
if (ServicePickerPreference.isNoneValue(uploaderComponent)) return null if (ComponentPickerPreference.isNoneValue(uploaderComponent)) return null
val uploader = MediaUploaderInterface.getInstance(app, uploaderComponent) ?: val uploader = MediaUploaderInterface.getInstance(app, uploaderComponent) ?:
throw UploaderNotFoundException(context.getString(R.string.error_message_media_uploader_not_found)) throw UploaderNotFoundException(context.getString(R.string.error_message_media_uploader_not_found))
try { try {

View File

@ -23,12 +23,10 @@ import android.text.Editable
import android.text.Spanned import android.text.Spanned
import android.text.TextWatcher import android.text.TextWatcher
import android.widget.TextView import android.widget.TextView
import org.mariotaku.twidere.extension.applyTo
import org.mariotaku.twidere.text.SafeSpannableStringBuilder import org.mariotaku.twidere.text.SafeSpannableStringBuilder
import org.mariotaku.twidere.util.EmojiSupportUtils
import org.mariotaku.twidere.util.ExternalThemeManager import org.mariotaku.twidere.util.ExternalThemeManager
import org.mariotaku.twidere.util.dagger.GeneralComponent import org.mariotaku.twidere.util.dagger.GeneralComponent
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -45,7 +43,8 @@ class EmojiEditableFactory(textView: TextView) : Editable.Factory() {
override fun newEditable(source: CharSequence): Editable { override fun newEditable(source: CharSequence): Editable {
val editable = SafeSpannableStringBuilder(source) val editable = SafeSpannableStringBuilder(source)
EmojiSupportUtils.applyEmoji(externalThemeManager, editable) val emoji = externalThemeManager.emoji
emoji?.applyTo(editable)
editable.setSpan(object : TextWatcher { editable.setSpan(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
@ -53,7 +52,7 @@ class EmojiEditableFactory(textView: TextView) : Editable.Factory() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (count <= 0) return if (count <= 0) return
EmojiSupportUtils.applyEmoji(externalThemeManager, editable, start, count) emoji?.applyTo(editable)
} }
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {

View File

@ -21,12 +21,10 @@ package org.mariotaku.twidere.text.util
import android.text.Spannable import android.text.Spannable
import android.widget.TextView import android.widget.TextView
import org.mariotaku.twidere.extension.applyTo
import org.mariotaku.twidere.text.SafeSpannableString import org.mariotaku.twidere.text.SafeSpannableString
import org.mariotaku.twidere.util.EmojiSupportUtils
import org.mariotaku.twidere.util.ExternalThemeManager import org.mariotaku.twidere.util.ExternalThemeManager
import org.mariotaku.twidere.util.dagger.GeneralComponent import org.mariotaku.twidere.util.dagger.GeneralComponent
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -43,7 +41,7 @@ class EmojiSpannableFactory(textView: TextView) : Spannable.Factory() {
override fun newSpannable(source: CharSequence): Spannable { override fun newSpannable(source: CharSequence): Spannable {
val spannable = SafeSpannableString(source) val spannable = SafeSpannableString(source)
EmojiSupportUtils.applyEmoji(externalThemeManager, spannable) externalThemeManager.emoji?.applyTo(spannable)
return spannable return spannable
} }
} }

View File

@ -1,159 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.Spanned
import android.widget.TextView
import org.mariotaku.commons.text.CodePointArray
import org.mariotaku.commons.text.get
import org.mariotaku.twidere.text.style.EmojiSpan
import org.mariotaku.twidere.text.util.EmojiEditableFactory
import org.mariotaku.twidere.text.util.EmojiSpannableFactory
/**
* Created by mariotaku on 15/12/20.
*/
object EmojiSupportUtils {
fun initForTextView(textView: TextView) {
if (textView.isInEditMode) return
textView.setSpannableFactory(EmojiSpannableFactory(textView))
textView.setEditableFactory(EmojiEditableFactory(textView))
}
fun applyEmoji(manager: ExternalThemeManager, text: Spannable,
textStart: Int = 0, textLength: Int = text.length) {
val emoji = manager.emoji
if (emoji == null || !emoji.isSupported) return
val array = CodePointArray(text)
var arrayIdx = array.length() - 1
while (arrayIdx >= 0) {
val codePoint = array[arrayIdx]
if (isEmoji(codePoint)) {
val arrayEnd = arrayIdx + 1
var arrayIdxOffset = 0
val textIdx = array.indexOfText(codePoint, arrayIdx)
var textIdxOffset = 0
var skippedIndex = 0
if (textIdx == -1 || textIdx < textStart) {
arrayIdx--
continue
}
val textEnd = textIdx + Character.charCount(codePoint)
if (arrayIdx > 0) {
val prevCodePoint = array[arrayIdx - 1]
when {
isRegionalIndicatorSymbol(codePoint) -> if (isRegionalIndicatorSymbol(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isModifier(codePoint) -> if (isEmoji(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isKeyCap(codePoint) -> if (isPhoneNumberSymbol(prevCodePoint)) {
arrayIdxOffset = -1
textIdxOffset = -Character.charCount(prevCodePoint)
skippedIndex = -1
}
isZeroWidthJoin(prevCodePoint) -> {
var notValidControlCount = 0
var charCount = 0
for (i in arrayIdx - 1 downTo 0) {
val cp = array.get(i)
charCount += Character.charCount(cp)
if (isZeroWidthJoin(cp) || isVariationSelector(cp)) {
// Ignore
notValidControlCount = 0
continue
}
notValidControlCount++
if (notValidControlCount > 1 || i == 0) {
arrayIdxOffset = i - arrayIdx + 1
textIdxOffset = -charCount + Character.charCount(cp)
skippedIndex = i - arrayIdx + 1
break
}
}
}
}
}
if (textEnd > textStart + textLength) {
arrayIdx--
continue
}
var spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java)
if (spans.isEmpty()) {
var drawable: Drawable? = emoji.getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd])
if (drawable == null) {
// Not emoji combination, just use fallback
textIdxOffset = 0
arrayIdxOffset = 0
skippedIndex = 0
spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java)
if (spans.isEmpty()) {
drawable = emoji.getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd])
}
}
if (drawable != null) {
text.setSpan(EmojiSpan(drawable), textIdx + textIdxOffset, textEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
arrayIdx += skippedIndex
}
arrayIdx--
}
}
private fun isVariationSelector(codePoint: Int): Boolean {
return codePoint == 0xfe0f
}
private fun isZeroWidthJoin(codePoint: Int): Boolean {
return codePoint == 0x200d
}
private fun isPhoneNumberSymbol(codePoint: Int): Boolean {
return codePoint == 0x0023 || codePoint == 0x002a || codePoint in 0x0030..0x0039
}
private fun isModifier(codePoint: Int): Boolean {
return codePoint in 0x1f3fb..0x1f3ff
}
private fun isEmoji(codePoint: Int): Boolean {
return !Character.isLetterOrDigit(codePoint)
}
private fun isRegionalIndicatorSymbol(codePoint: Int): Boolean {
return codePoint in 0x1f1e6..0x1f1ff
}
private fun isKeyCap(codePoint: Int): Boolean {
return codePoint == 0x20e3
}
}

View File

@ -28,7 +28,6 @@ import android.graphics.drawable.Drawable
import android.support.v4.content.res.ResourcesCompat import android.support.v4.content.res.ResourcesCompat
import android.util.LruCache import android.util.LruCache
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_EMOJI_SUPPORT import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_EMOJI_SUPPORT
import java.util.*
/** /**
* Created by mariotaku on 15/12/20. * Created by mariotaku on 15/12/20.
@ -105,7 +104,11 @@ class ExternalThemeManager(private val context: Context, private val preferences
if (i != 0) { if (i != 0) {
sb.append("_") sb.append("_")
} }
sb.append(String.format(Locale.US, "%04x", codePoints[i])) val hex = Integer.toHexString(codePoints[i])
for (j in 0 until 4 - hex.length) {
sb.append("0")
}
sb.append(hex)
} }
val identifier = resources.getIdentifier(sb.toString(), val identifier = resources.getIdentifier(sb.toString(),
if (useMipmap) "mipmap" else "drawable", packageName) if (useMipmap) "mipmap" else "drawable", packageName)

View File

@ -151,8 +151,8 @@ interface GeneralComponent {
fun inject(service: BaseService) fun inject(service: BaseService)
companion object { companion object {
private var instance: GeneralComponent? = null
private var instance: GeneralComponent? = null
fun get(context: Context): GeneralComponent { fun get(context: Context): GeneralComponent {
return instance ?: run { return instance ?: run {
val helper = DaggerGeneralComponent.builder().applicationModule(ApplicationModule.get(context)).build() val helper = DaggerGeneralComponent.builder().applicationModule(ApplicationModule.get(context)).build()
@ -160,5 +160,6 @@ interface GeneralComponent {
return@run helper return@run helper
} }
} }
} }
} }

View File

@ -36,8 +36,8 @@ import com.bumptech.glide.Glide
import org.mariotaku.chameleon.view.ChameleonMultiAutoCompleteTextView import org.mariotaku.chameleon.view.ChameleonMultiAutoCompleteTextView
import org.mariotaku.ktextension.contains import org.mariotaku.ktextension.contains
import org.mariotaku.twidere.adapter.ComposeAutoCompleteAdapter import org.mariotaku.twidere.adapter.ComposeAutoCompleteAdapter
import org.mariotaku.twidere.extension.setupEmojiFactory
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.EmojiSupportUtils
import org.mariotaku.twidere.util.widget.StatusTextTokenizer import org.mariotaku.twidere.util.widget.StatusTextTokenizer
@ -55,7 +55,7 @@ class ComposeEditText(
} }
init { init {
EmojiSupportUtils.initForTextView(this) setupEmojiFactory()
setTokenizer(StatusTextTokenizer()) setTokenizer(StatusTextTokenizer())
onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
removeIMESuggestions() removeIMESuggestions()

View File

@ -3,8 +3,7 @@ package org.mariotaku.twidere.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import org.mariotaku.chameleon.view.ChameleonEditText import org.mariotaku.chameleon.view.ChameleonEditText
import org.mariotaku.twidere.text.util.SafeEditableFactory import org.mariotaku.twidere.extension.setupEmojiFactory
import org.mariotaku.twidere.text.util.SafeSpannableFactory
/** /**
* Created by mariotaku on 2017/2/3. * Created by mariotaku on 2017/2/3.
@ -13,8 +12,7 @@ import org.mariotaku.twidere.text.util.SafeSpannableFactory
class FixedEditText(context: Context, attrs: AttributeSet? = null) : ChameleonEditText(context, attrs) { class FixedEditText(context: Context, attrs: AttributeSet? = null) : ChameleonEditText(context, attrs) {
init { init {
setSpannableFactory(SafeSpannableFactory) setupEmojiFactory()
setEditableFactory(SafeEditableFactory)
} }
override fun onTextContextMenuItem(id: Int): Boolean { override fun onTextContextMenuItem(id: Int): Boolean {

View File

@ -3,8 +3,7 @@ package org.mariotaku.twidere.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import org.mariotaku.chameleon.view.ChameleonTextView import org.mariotaku.chameleon.view.ChameleonTextView
import org.mariotaku.twidere.text.util.SafeEditableFactory import org.mariotaku.twidere.extension.setupEmojiFactory
import org.mariotaku.twidere.text.util.SafeSpannableFactory
/** /**
* Created by mariotaku on 2017/2/3. * Created by mariotaku on 2017/2/3.
@ -13,8 +12,7 @@ import org.mariotaku.twidere.text.util.SafeSpannableFactory
open class FixedTextView(context: Context, attrs: AttributeSet? = null) : ChameleonTextView(context, attrs) { open class FixedTextView(context: Context, attrs: AttributeSet? = null) : ChameleonTextView(context, attrs) {
init { init {
setSpannableFactory(SafeSpannableFactory) setupEmojiFactory()
setEditableFactory(SafeEditableFactory)
} }
override fun onTextContextMenuItem(id: Int): Boolean { override fun onTextContextMenuItem(id: Int): Boolean {
@ -25,4 +23,5 @@ open class FixedTextView(context: Context, attrs: AttributeSet? = null) : Chamel
return true return true
} }
} }
} }

View File

@ -31,7 +31,6 @@ import android.text.style.StyleSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.util.EmojiSupportUtils
/** /**
* Created by mariotaku on 15/5/28. * Created by mariotaku on 15/5/28.
@ -60,7 +59,6 @@ class NameView(context: Context, attrs: AttributeSet? = null) : FixedTextView(co
private var secondaryTextSize: AbsoluteSizeSpan? = null private var secondaryTextSize: AbsoluteSizeSpan? = null
init { init {
EmojiSupportUtils.initForTextView(this)
ellipsize = TextUtils.TruncateAt.END ellipsize = TextUtils.TruncateAt.END
val a = context.obtainStyledAttributes(attrs, R.styleable.NameView, 0, 0) val a = context.obtainStyledAttributes(attrs, R.styleable.NameView, 0, 0)
setPrimaryTextColor(a.getColor(R.styleable.NameView_nv_primaryTextColor, 0)) setPrimaryTextColor(a.getColor(R.styleable.NameView_nv_primaryTextColor, 0))
@ -123,7 +121,7 @@ class NameView(context: Context, attrs: AttributeSet? = null) : FixedTextView(co
sb.setSpan(secondaryTextStyle, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) sb.setSpan(secondaryTextStyle, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(secondaryTextSize, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) sb.setSpan(secondaryTextSize, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
text = sb setText(sb, BufferType.SPANNABLE)
} }
fun setPrimaryTextSize(textSize: Float) { fun setPrimaryTextSize(textSize: Float) {

View File

@ -28,7 +28,7 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
import org.mariotaku.chameleon.view.ChameleonTextView import org.mariotaku.chameleon.view.ChameleonTextView
import org.mariotaku.twidere.util.EmojiSupportUtils import org.mariotaku.twidere.extension.setupEmojiFactory
/** /**
* Returns true when not clicking links * Returns true when not clicking links
@ -41,7 +41,7 @@ class TimelineContentTextView @JvmOverloads constructor(
) : ChameleonTextView(context, attrs, defStyle) { ) : ChameleonTextView(context, attrs, defStyle) {
init { init {
EmojiSupportUtils.initForTextView(this) setupEmojiFactory()
} }
override fun dispatchTouchEvent(event: MotionEvent): Boolean { override fun dispatchTouchEvent(event: MotionEvent): Boolean {

View File

@ -34,6 +34,7 @@ import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.mariotaku.abstask.library.AbstractTask import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.abstask.library.TaskStarter import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.ktextension.spannable
import org.mariotaku.ktextension.toLongOr import org.mariotaku.ktextension.toLongOr
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterCaps import org.mariotaku.microblog.library.twitter.TwitterCaps
@ -189,7 +190,7 @@ class CardPollViewController : ContainerView.ViewController() {
val value = card.getAsInteger("choice${choiceIndex}_count", 0) val value = card.getAsInteger("choice${choiceIndex}_count", 0)
if (label == null) throw NullPointerException() if (label == null) throw NullPointerException()
val choicePercent = if (votesSum == 0) 0f else value / votesSum.toFloat() val choicePercent = if (votesSum == 0) 0f else value / votesSum.toFloat()
choiceLabelView.text = label choiceLabelView.spannable = label
choicePercentView.text = String.format(Locale.US, "%d%%", Math.round(choicePercent * 100)) choicePercentView.text = String.format(Locale.US, "%d%%", Math.round(choicePercent * 100))
pollItem.setOnClickListener(clickListener) pollItem.setOnClickListener(clickListener)
@ -214,7 +215,7 @@ class CardPollViewController : ContainerView.ViewController() {
val nVotes = context.resources.getQuantityString(R.plurals.N_votes, votesSum, votesSum) val nVotes = context.resources.getQuantityString(R.plurals.N_votes, votesSum, votesSum)
val timeLeft = DateUtils.getRelativeTimeSpanString(context, endDatetimeUtc.time, true) val timeLeft = DateUtils.getRelativeTimeSpanString(context, endDatetimeUtc.time, true)
view.pollSummary.text = context.getString(R.string.poll_summary_format, nVotes, timeLeft) view.pollSummary.spannable = context.getString(R.string.poll_summary_format, nVotes, timeLeft)
} }
private class PercentDrawable internal constructor( private class PercentDrawable internal constructor(

View File

@ -19,13 +19,12 @@
package org.mariotaku.twidere.view.holder package org.mariotaku.twidere.view.holder
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.AccountDetailsAdapter import org.mariotaku.twidere.adapter.AccountDetailsAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -60,10 +59,9 @@ class AccountViewHolder(
dragHandle.visibility = if (enabled) View.VISIBLE else View.GONE dragHandle.visibility = if (enabled) View.VISIBLE else View.GONE
} }
@SuppressLint("SetTextI18n")
fun display(details: AccountDetails) { fun display(details: AccountDetails) {
name.text = details.user.name name.spannable = details.user.name
screenName.text = "@${details.user.screen_name}" screenName.spannable = "@${details.user.screen_name}"
setAccountColor(details.color) setAccountColor(details.color)
profileImage.visibility = View.VISIBLE profileImage.visibility = View.VISIBLE
adapter.requestManager.loadProfileImage(adapter.context, details, adapter.profileImageStyle, adapter.requestManager.loadProfileImage(adapter.context, details, adapter.profileImageStyle,

View File

@ -25,6 +25,7 @@ import android.view.View
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import kotlinx.android.synthetic.main.list_item_draft.view.* import kotlinx.android.synthetic.main.list_item_draft.view.*
import org.mariotaku.ktextension.mapToArray import org.mariotaku.ktextension.mapToArray
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.model.getActionName import org.mariotaku.twidere.extension.model.getActionName
import org.mariotaku.twidere.model.Draft import org.mariotaku.twidere.model.Draft
@ -76,11 +77,11 @@ class DraftViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
contentView.drawEnd() contentView.drawEnd()
} }
if (summaryText != null) { if (summaryText != null) {
textView.text = summaryText textView.spannable = summaryText
} else if (draft.text.isNullOrEmpty()) { } else if (draft.text.isNullOrEmpty()) {
textView.setText(R.string.empty_content) textView.setText(R.string.empty_content)
} else { } else {
textView.text = draft.text textView.spannable = draft.text
} }
if (draft.timestamp > 0) { if (draft.timestamp > 0) {

View File

@ -20,9 +20,10 @@
package org.mariotaku.twidere.view.holder package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView.ViewHolder import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.card_item_group_compact.view.* import kotlinx.android.synthetic.main.card_item_group_compact.view.*
import org.mariotaku.ktextension.hideIfEmpty
import org.mariotaku.ktextension.spannable
import org.mariotaku.ktextension.toLocalizedString import org.mariotaku.ktextension.toLocalizedString
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IGroupsAdapter import org.mariotaku.twidere.adapter.iface.IGroupsAdapter
@ -63,7 +64,7 @@ class GroupViewHolder(private val adapter: IGroupsAdapter<*>, itemView: View) :
externalIndicator.visibility = View.GONE externalIndicator.visibility = View.GONE
} else { } else {
externalIndicator.visibility = View.VISIBLE externalIndicator.visibility = View.VISIBLE
externalIndicator.text = context.getString(R.string.external_group_host_format, externalIndicator.spannable = context.getString(R.string.external_group_host_format,
groupHost) groupHost)
} }
if (adapter.profileImageEnabled) { if (adapter.profileImageEnabled) {
@ -74,8 +75,8 @@ class GroupViewHolder(private val adapter: IGroupsAdapter<*>, itemView: View) :
} else { } else {
profileImageView.visibility = View.GONE profileImageView.visibility = View.GONE
} }
descriptionView.visibility = if (TextUtils.isEmpty(group.description)) View.GONE else View.VISIBLE descriptionView.spannable = formatter.unicodeWrap(group.description)
descriptionView.text = formatter.unicodeWrap(group.description) descriptionView.hideIfEmpty()
membersCountView.text = group.member_count.toLocalizedString() membersCountView.text = group.member_count.toLocalizedString()
adminsCountView.text = group.admin_count.toLocalizedString() adminsCountView.text = group.admin_count.toLocalizedString()
} }

View File

@ -24,6 +24,7 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.commonsware.cwac.layouts.AspectLockedFrameLayout import com.commonsware.cwac.layouts.AspectLockedFrameLayout
import kotlinx.android.synthetic.main.adapter_item_media_status.view.* import kotlinx.android.synthetic.main.adapter_item_media_status.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -62,9 +63,9 @@ class MediaStatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView:
val displayEnd = status.extras?.display_text_range?.getOrNull(1) ?: -1 val displayEnd = status.extras?.display_text_range?.getOrNull(1) ?: -1
if (displayEnd >= 0) { if (displayEnd >= 0) {
mediaTextView.text = status.text_unescaped.subSequence(0, displayEnd) mediaTextView.spannable = status.text_unescaped.subSequence(0, displayEnd)
} else { } else {
mediaTextView.text = status.text_unescaped mediaTextView.spannable = status.text_unescaped
} }
adapter.requestManager.loadProfileImage(context, status, adapter.requestManager.loadProfileImage(context, status,
adapter.profileImageStyle, profileImageView.cornerRadius, adapter.profileImageStyle, profileImageView.cornerRadius,

View File

@ -4,6 +4,7 @@ import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import kotlinx.android.synthetic.main.list_item_simple_user_list.view.* import kotlinx.android.synthetic.main.list_item_simple_user_list.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IUserListsAdapter import org.mariotaku.twidere.adapter.iface.IUserListsAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -28,8 +29,8 @@ class SimpleUserListViewHolder(
} }
fun display(userList: ParcelableUserList) { fun display(userList: ParcelableUserList) {
nameView.text = userList.name nameView.spannable = userList.name
createdByView.text = createdByView.context.getString(R.string.created_by, createdByView.spannable = createdByView.context.getString(R.string.created_by,
adapter.userColorNameManager.getDisplayName(userList, false)) adapter.userColorNameManager.getDisplayName(userList, false))
if (adapter.profileImageEnabled) { if (adapter.profileImageEnabled) {
profileImageView.visibility = View.VISIBLE profileImageView.visibility = View.VISIBLE

View File

@ -5,6 +5,7 @@ import android.view.View
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.TextView import android.widget.TextView
import kotlinx.android.synthetic.main.list_item_simple_user.view.* import kotlinx.android.synthetic.main.list_item_simple_user.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IContentAdapter import org.mariotaku.twidere.adapter.iface.IContentAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -30,8 +31,8 @@ open class SimpleUserViewHolder<out A : IContentAdapter>(
} }
open fun displayUser(user: ParcelableUser) { open fun displayUser(user: ParcelableUser) {
nameView.text = user.name nameView.spannable = user.name
secondaryNameView.text = "@${user.screen_name}" secondaryNameView.spannable = "@${user.screen_name}"
if (adapter.profileImageEnabled) { if (adapter.profileImageEnabled) {
val context = itemView.context val context = itemView.context
adapter.requestManager.loadProfileImage(context, user, adapter.profileImageStyle, adapter.requestManager.loadProfileImage(context, user, adapter.profileImageStyle,

View File

@ -17,10 +17,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import kotlinx.android.synthetic.main.list_item_status.view.* import kotlinx.android.synthetic.main.list_item_status.view.*
import org.mariotaku.ktextension.applyFontFamily import org.mariotaku.ktextension.*
import org.mariotaku.ktextension.empty
import org.mariotaku.ktextension.hideIfEmpty
import org.mariotaku.ktextension.isNotNullOrEmpty
import org.mariotaku.microblog.library.mastodon.annotation.StatusVisibility import org.mariotaku.microblog.library.mastodon.annotation.StatusVisibility
import org.mariotaku.twidere.Constants.* import org.mariotaku.twidere.Constants.*
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
@ -116,12 +113,12 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
nameView.updateText(adapter.bidiFormatter) nameView.updateText(adapter.bidiFormatter)
summaryView.hideIfEmpty() summaryView.hideIfEmpty()
if (adapter.linkHighlightingStyle == VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) { if (adapter.linkHighlightingStyle == VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) {
textView.text = toPlainText(TWIDERE_PREVIEW_TEXT_HTML) textView.spannable = toPlainText(TWIDERE_PREVIEW_TEXT_HTML)
} else { } else {
val linkify = adapter.twidereLinkify val linkify = adapter.twidereLinkify
val text = HtmlSpanBuilder.fromHtml(TWIDERE_PREVIEW_TEXT_HTML) val text = HtmlSpanBuilder.fromHtml(TWIDERE_PREVIEW_TEXT_HTML)
linkify.applyAllLinks(text, null, -1, false, adapter.linkHighlightingStyle, true) linkify.applyAllLinks(text, null, -1, false, adapter.linkHighlightingStyle, true)
textView.text = text textView.spannable = text
} }
timeView.time = System.currentTimeMillis() timeView.time = System.currentTimeMillis()
val showCardActions = isCardActionsShown val showCardActions = isCardActionsShown
@ -177,7 +174,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
} else if (status.retweet_id != null) { } else if (status.retweet_id != null) {
val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!, val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!,
status.retweeted_by_user_name, status.retweeted_by_user_acct!!, nameFirst) status.retweeted_by_user_name, status.retweeted_by_user_acct!!, nameFirst)
statusInfoLabel.text = context.getString(R.string.name_retweeted, formatter.unicodeWrap(retweetedBy)) statusInfoLabel.spannable = context.getString(R.string.name_retweeted, formatter.unicodeWrap(retweetedBy))
statusInfoIcon.setImageResource(R.drawable.ic_activity_action_retweet) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_retweet)
statusInfoLabel.visibility = View.VISIBLE statusInfoLabel.visibility = View.VISIBLE
statusInfoIcon.visibility = View.VISIBLE statusInfoIcon.visibility = View.VISIBLE
@ -187,9 +184,9 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
if (status.in_reply_to_name != null && status.in_reply_to_screen_name != null) { if (status.in_reply_to_name != null && status.in_reply_to_screen_name != null) {
val inReplyTo = colorNameManager.getDisplayName(status.in_reply_to_user_key!!, val inReplyTo = colorNameManager.getDisplayName(status.in_reply_to_user_key!!,
status.in_reply_to_name, status.in_reply_to_screen_name, nameFirst) status.in_reply_to_name, status.in_reply_to_screen_name, nameFirst)
statusInfoLabel.text = context.getString(R.string.in_reply_to_name, formatter.unicodeWrap(inReplyTo)) statusInfoLabel.spannable = context.getString(R.string.in_reply_to_name, formatter.unicodeWrap(inReplyTo))
} else { } else {
statusInfoLabel.text = context.getString(R.string.label_status_type_reply) statusInfoLabel.spannable = context.getString(R.string.label_status_type_reply)
} }
statusInfoIcon.setImageResource(R.drawable.ic_activity_action_reply) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_reply)
statusInfoLabel.visibility = View.VISIBLE statusInfoLabel.visibility = View.VISIBLE
@ -231,9 +228,9 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
quotedText = status.quoted_text_unescaped quotedText = status.quoted_text_unescaped
} }
if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) {
quotedTextView.text = quotedText.subSequence(0, quotedDisplayEnd) quotedTextView.spannable = quotedText.subSequence(0, quotedDisplayEnd)
} else { } else {
quotedTextView.text = quotedText quotedTextView.spannable = quotedText
} }
if (quotedTextView.length() == 0) { if (quotedTextView.length() == 0) {
@ -263,7 +260,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
quotedMediaLabel.visibility = View.GONE quotedMediaLabel.visibility = View.GONE
} }
quotedTextView.text = if (!quoteContentAvailable) { quotedTextView.spannable = if (!quoteContentAvailable) {
// Display 'not available' label // Display 'not available' label
SpannableString.valueOf(context.getString(R.string.label_status_not_available)).apply { SpannableString.valueOf(context.getString(R.string.label_status_not_available)).apply {
setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context,
@ -359,7 +356,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
mediaPreview.visibility = View.GONE mediaPreview.visibility = View.GONE
} }
summaryView.text = status.extras?.summary_text summaryView.spannable = status.extras?.summary_text
summaryView.hideIfEmpty() summaryView.hideIfEmpty()
val text: CharSequence val text: CharSequence
@ -385,19 +382,18 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
} }
if (displayEnd != -1 && displayEnd <= text.length) { if (displayEnd != -1 && displayEnd <= text.length) {
textView.text = text.subSequence(0, displayEnd) textView.spannable = text.subSequence(0, displayEnd)
} else { } else {
textView.text = text textView.spannable = text
} }
textView.hideIfEmpty() textView.hideIfEmpty()
if (replyCount > 0) { if (replyCount > 0) {
replyCountView.text = UnitConvertUtils.calculateProperCount(replyCount) replyCountView.spannable = UnitConvertUtils.calculateProperCount(replyCount)
replyCountView.visibility = View.VISIBLE
} else { } else {
replyCountView.text = null replyCountView.spannable = null
replyCountView.visibility = View.GONE
} }
replyCountView.hideIfEmpty()
when (status.extras?.visibility) { when (status.extras?.visibility) {
StatusVisibility.PRIVATE -> { StatusVisibility.PRIVATE -> {
@ -428,12 +424,12 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
retweetCount = status.retweet_count retweetCount = status.retweet_count
if (retweetCount > 0) { if (retweetCount > 0) {
retweetCountView.text = UnitConvertUtils.calculateProperCount(retweetCount) retweetCountView.spannable = UnitConvertUtils.calculateProperCount(retweetCount)
retweetCountView.visibility = View.VISIBLE
} else { } else {
retweetCountView.text = null retweetCountView.spannable = null
retweetCountView.visibility = View.GONE
} }
retweetCountView.hideIfEmpty()
if (DestroyFavoriteTask.isDestroyingFavorite(status.account_key, status.id)) { if (DestroyFavoriteTask.isDestroyingFavorite(status.account_key, status.id)) {
favoriteIcon.isActivated = false favoriteIcon.isActivated = false
} else { } else {
@ -441,13 +437,13 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
favoriteIcon.isActivated = creatingFavorite || status.is_favorite favoriteIcon.isActivated = creatingFavorite || status.is_favorite
} }
favoriteCount = status.favorite_count favoriteCount = status.favorite_count
if (favoriteCount > 0) { if (favoriteCount > 0) {
favoriteCountView.text = UnitConvertUtils.calculateProperCount(favoriteCount) favoriteCountView.spannable = UnitConvertUtils.calculateProperCount(favoriteCount)
favoriteCountView.visibility = View.VISIBLE
} else { } else {
favoriteCountView.text = null favoriteCountView.spannable = null
favoriteCountView.visibility = View.GONE
} }
favoriteCountView.hideIfEmpty()
nameView.updateText(formatter) nameView.updateText(formatter)
quotedNameView.updateText(formatter) quotedNameView.updateText(formatter)

View File

@ -20,10 +20,11 @@
package org.mariotaku.twidere.view.holder package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView.ViewHolder import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import kotlinx.android.synthetic.main.list_item_user_list.view.* import kotlinx.android.synthetic.main.list_item_user_list.view.*
import org.mariotaku.ktextension.hideIfEmpty
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IUserListsAdapter import org.mariotaku.twidere.adapter.iface.IUserListsAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -68,10 +69,10 @@ class UserListViewHolder(
val manager = adapter.userColorNameManager val manager = adapter.userColorNameManager
itemContent.drawStart(manager.getUserColor(userList.user_key)) itemContent.drawStart(manager.getUserColor(userList.user_key))
nameView.text = userList.name nameView.spannable = userList.name
val nameFirst = adapter.nameFirst val nameFirst = adapter.nameFirst
val createdByDisplayName = manager.getDisplayName(userList, nameFirst) val createdByDisplayName = manager.getDisplayName(userList, nameFirst)
createdByView.text = context.getString(R.string.created_by, createdByDisplayName) createdByView.spannable = context.getString(R.string.created_by, createdByDisplayName)
if (adapter.profileImageEnabled) { if (adapter.profileImageEnabled) {
profileImageView.visibility = View.VISIBLE profileImageView.visibility = View.VISIBLE
@ -80,8 +81,8 @@ class UserListViewHolder(
} else { } else {
profileImageView.visibility = View.GONE profileImageView.visibility = View.GONE
} }
descriptionView.visibility = if (TextUtils.isEmpty(userList.description)) View.GONE else View.VISIBLE descriptionView.spannable = userList.description
descriptionView.text = userList.description descriptionView.hideIfEmpty()
membersCountView.text = Utils.getLocalizedNumber(Locale.getDefault(), userList.members_count) membersCountView.text = Utils.getLocalizedNumber(Locale.getDefault(), userList.members_count)
subscribersCountView.text = Utils.getLocalizedNumber(Locale.getDefault(), userList.subscribers_count) subscribersCountView.text = Utils.getLocalizedNumber(Locale.getDefault(), userList.subscribers_count)
} }

View File

@ -26,6 +26,7 @@ import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import android.widget.RelativeLayout import android.widget.RelativeLayout
import kotlinx.android.synthetic.main.list_item_user.view.* import kotlinx.android.synthetic.main.list_item_user.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IUsersAdapter import org.mariotaku.twidere.adapter.iface.IUsersAdapter
import org.mariotaku.twidere.adapter.iface.IUsersAdapter.* import org.mariotaku.twidere.adapter.iface.IUsersAdapter.*
@ -166,11 +167,11 @@ class UserViewHolder(
if (!simple) { if (!simple) {
descriptionView.visibility = if (TextUtils.isEmpty(user.description_unescaped)) View.GONE else View.VISIBLE descriptionView.visibility = if (TextUtils.isEmpty(user.description_unescaped)) View.GONE else View.VISIBLE
descriptionView.text = user.description_unescaped descriptionView.spannable = user.description_unescaped
locationView.visibility = if (TextUtils.isEmpty(user.location)) View.GONE else View.VISIBLE locationView.visibility = if (TextUtils.isEmpty(user.location)) View.GONE else View.VISIBLE
locationView.text = user.location locationView.spannable = user.location
urlView.visibility = if (TextUtils.isEmpty(user.url_expanded)) View.GONE else View.VISIBLE urlView.visibility = if (TextUtils.isEmpty(user.url_expanded)) View.GONE else View.VISIBLE
urlView.text = user.url_expanded urlView.spannable = user.url_expanded
val locale = Locale.getDefault() val locale = Locale.getDefault()
statusesCountView.text = Utils.getLocalizedNumber(locale, user.statuses_count) statusesCountView.text = Utils.getLocalizedNumber(locale, user.statuses_count)
followersCountView.text = Utils.getLocalizedNumber(locale, user.followers_count) followersCountView.text = Utils.getLocalizedNumber(locale, user.followers_count)

View File

@ -22,6 +22,7 @@ package org.mariotaku.twidere.view.holder.message
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.list_item_message_entry.view.* import kotlinx.android.synthetic.main.list_item_message_entry.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesEntriesAdapter import org.mariotaku.twidere.adapter.MessagesEntriesAdapter
import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.loadProfileImage
@ -80,8 +81,8 @@ class MessageEntryViewHolder(itemView: View, val adapter: MessagesEntriesAdapter
this.name.name = name this.name.name = name
this.name.screenName = secondaryName this.name.screenName = secondaryName
this.name.updateText(adapter.bidiFormatter) this.name.updateText(adapter.bidiFormatter)
this.text.text = conversation.getSummaryText(itemView.context, adapter.userColorNameManager, this.text.spannable = conversation.getSummaryText(itemView.context,
adapter.nameFirst) adapter.userColorNameManager, adapter.nameFirst)
if (conversation.is_outgoing) { if (conversation.is_outgoing) {
readIndicator.visibility = View.VISIBLE readIndicator.visibility = View.VISIBLE
readIndicator.setImageResource(R.drawable.ic_message_type_outgoing) readIndicator.setImageResource(R.drawable.ic_message_type_outgoing)

View File

@ -24,6 +24,7 @@ import android.view.View
import kotlinx.android.synthetic.main.list_item_message_conversation_text.view.* import kotlinx.android.synthetic.main.list_item_message_conversation_text.view.*
import org.mariotaku.ktextension.empty import org.mariotaku.ktextension.empty
import org.mariotaku.ktextension.isNullOrEmpty import org.mariotaku.ktextension.isNullOrEmpty
import org.mariotaku.ktextension.spannable
import org.mariotaku.messagebubbleview.library.MessageBubbleView import org.mariotaku.messagebubbleview.library.MessageBubbleView
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesConversationAdapter import org.mariotaku.twidere.adapter.MessagesConversationAdapter
@ -92,7 +93,7 @@ class MessageViewHolder(itemView: View, adapter: MessagesConversationAdapter) :
} }
text.text = SpannableStringBuilder.valueOf(message.text_unescaped).apply { text.spannable = SpannableStringBuilder.valueOf(message.text_unescaped).apply {
message.spans?.applyTo(this) message.spans?.applyTo(this)
adapter.linkify.applyAllLinks(this, message.account_key, layoutPosition.toLong(), adapter.linkify.applyAllLinks(this, message.account_key, layoutPosition.toLong(),
false, adapter.linkHighlightingStyle, true) false, adapter.linkHighlightingStyle, true)

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.view.holder.message
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.list_item_message_conversation_notice.view.* import kotlinx.android.synthetic.main.list_item_message_conversation_notice.view.*
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesConversationAdapter import org.mariotaku.twidere.adapter.MessagesConversationAdapter
import org.mariotaku.twidere.extension.model.getSummaryText import org.mariotaku.twidere.extension.model.getSummaryText
@ -35,7 +36,7 @@ class NoticeSummaryEventViewHolder(itemView: View, adapter: MessagesConversation
override fun display(message: ParcelableMessage, showDate: Boolean) { override fun display(message: ParcelableMessage, showDate: Boolean) {
super.display(message, showDate) super.display(message, showDate)
text.text = message.getSummaryText(adapter.context, adapter.userColorNameManager, text.spannable = message.getSummaryText(adapter.context, adapter.userColorNameManager,
adapter.conversation, adapter.nameFirst) adapter.conversation, adapter.nameFirst)
} }

View File

@ -432,7 +432,7 @@
<string name="label_refresh_and_sync_service">刷新和同步服务</string> <string name="label_refresh_and_sync_service">刷新和同步服务</string>
<string name="label_refresh_service">刷新服务</string> <string name="label_refresh_service">刷新服务</string>
<string name="label_reply_name">回复 <xliff:g id="user_name">%s</xliff:g></string> <string name="label_reply_name">回复 <xliff:g id="user_name">%s</xliff:g></string>
<string name="label_reply_name_text">引用 <xliff:g id="name">%1$s</xliff:g><xliff:g id="text">%2$s</xliff:g></string> <string name="label_reply_name_text">回复 <xliff:g id="name">%1$s</xliff:g><xliff:g id="text">%2$s</xliff:g></string>
<string name="label_schedule_time_buffer_default">Buffer 默认</string> <string name="label_schedule_time_buffer_default">Buffer 默认</string>
<string name="label_send_at">发送于</string> <string name="label_send_at">发送于</string>
<string name="label_sensitive_content">敏感内容</string> <string name="label_sensitive_content">敏感内容</string>

View File

@ -50,7 +50,7 @@
tools:tint="?android:textColorSecondary" tools:tint="?android:textColorSecondary"
tools:visibility="visible"/> tools:visibility="visible"/>
<org.mariotaku.twidere.view.ActionIconThemedTextView <org.mariotaku.twidere.view.FixedTextView
android:id="@+id/statusInfoLabel" android:id="@+id/statusInfoLabel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"