Load custom emoji in statuses (#400)

This commit is contained in:
Ivan Kupalov 2017-10-19 17:25:04 +04:00 committed by Konrad Pozniak
parent 2859a5075c
commit 3adef27bbb
6 changed files with 199 additions and 82 deletions

View file

@ -49,6 +49,7 @@ dependencies {
compile "com.squareup.retrofit2:converter-gson:2.3.0"
compile "com.squareup.picasso:picasso:2.5.2"
compile "com.squareup.okhttp3:okhttp:3.9.0"
compile 'com.squareup.okhttp3:logging-interceptor:3.9.0'
compile "com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0"
compile "com.pkmmte.view:circularimageview:1.1"
compile "com.github.varunest:sparkbutton:1.0.5"

View file

@ -50,6 +50,7 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@ -126,13 +127,12 @@ public class BaseActivity extends AppCompatActivity {
protected void createMastodonApi() {
mastodonApiDispatcher = new Dispatcher();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
Gson gson = new GsonBuilder().registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
.create();
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
OkHttpClient.Builder okBuilder =
OkHttpUtils.getCompatibleClientBuilder().addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
@ -140,20 +140,21 @@ public class BaseActivity extends AppCompatActivity {
Request.Builder builder = originalRequest.newBuilder();
String accessToken = getAccessToken();
if (accessToken != null) {
builder.header("Authorization", String.format("Bearer %s",
accessToken));
builder.header("Authorization", String.format("Bearer %s", accessToken));
}
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
})
.dispatcher(mastodonApiDispatcher)
.build();
}).dispatcher(mastodonApiDispatcher);
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getBaseUrl())
.client(okHttpClient)
if (BuildConfig.DEBUG) {
okBuilder.addInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
}
Retrofit retrofit = new Retrofit.Builder().baseUrl(getBaseUrl())
.client(okBuilder.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
@ -161,11 +162,11 @@ public class BaseActivity extends AppCompatActivity {
}
protected void createTuskyApi() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
Retrofit retrofit =
new Retrofit.Builder().baseUrl("https://" + getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
tuskyApi = retrofit.create(TuskyApi.class);
}
@ -208,27 +209,25 @@ public class BaseActivity extends AppCompatActivity {
long checkInterval = 1000 * 60 * minutes;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, PullNotificationService.class);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(),
checkInterval, serviceAlarmIntent);
}
protected void disablePushNotifications() {
// Cancel the repeating call for "pull" notifications.
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, PullNotificationService.class);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(serviceAlarmIntent);
}
protected void clearNotifications() {
SharedPreferences notificationPreferences = getApplicationContext()
.getSharedPreferences("Notifications", MODE_PRIVATE);
notificationPreferences.edit()
.putString("current", "[]")
.apply();
SharedPreferences notificationPreferences =
getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
notificationPreferences.edit().putString("current", "[]").apply();
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(PullNotificationService.NOTIFY_ID);
@ -238,10 +237,10 @@ public class BaseActivity extends AppCompatActivity {
long checkInterval = 1000 * 60 * minutes;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, PullNotificationService.class);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(serviceAlarmIntent);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(),
checkInterval, serviceAlarmIntent);
}
}

View file

@ -2,6 +2,11 @@ package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
@ -9,7 +14,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ReplacementSpan;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageButton;
@ -25,11 +32,17 @@ import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.RoundedTransformation;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import com.varunest.sparkbutton.SparkButton;
import com.varunest.sparkbutton.SparkEventListener;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private View container;
@ -80,10 +93,8 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
videoIndicator = itemView.findViewById(R.id.status_video_indicator);
mediaLabel = itemView.findViewById(R.id.status_media_label);
contentWarningBar = itemView.findViewById(R.id.status_content_warning_bar);
contentWarningDescription =
itemView.findViewById(R.id.status_content_warning_description);
contentWarningButton =
itemView.findViewById(R.id.status_content_warning_button);
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
}
private void setDisplayName(String name) {
@ -97,14 +108,43 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
username.setText(usernameText);
}
private void setContent(Spanned content, Status.Mention[] mentions,
private Callback spanCallback = new Callback() {
@Override
public void onSuccess() {
content.invalidate();
}
@Override
public void onError() {
}
};
private void setContent(Spanned content, Status.Mention[] mentions, List<Status.Emoji> emojis,
StatusActionListener listener) {
SpannableStringBuilder builder = new SpannableStringBuilder(content);
if (!emojis.isEmpty()) {
CharSequence text = builder.subSequence(0, builder.length());
for (Status.Emoji emoji : emojis) {
CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':');
Matcher matcher = Pattern.compile(pattern.toString()).matcher(text);
while (matcher.find()) {
EmojiSpan span = new EmojiSpan();
span.setCallback(spanCallback);
builder.setSpan(span, matcher.start(), matcher.end(), 0);
Picasso.with(container.getContext())
.load(emoji.getUrl())
.into(span);
}
}
}
/* Redirect URLSpan's in the status content to the listener for viewing tag pages and
* account pages. */
Context context = this.content.getContext();
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("customTabs", true);
LinkHelper.setClickableText(this.content, content, mentions, useCustomTabs, listener);
boolean useCustomTabs =
PreferenceManager.getDefaultSharedPreferences(context).getBoolean("customTabs", true);
LinkHelper.setClickableText(this.content, builder, mentions, useCustomTabs, listener);
}
void setAvatar(String url, @Nullable String rebloggedUrl) {
@ -177,15 +217,13 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive,
final StatusActionListener listener, boolean showingSensitive) {
final ImageView[] previews = {
mediaPreview0,
mediaPreview1,
mediaPreview2,
mediaPreview3
mediaPreview0, mediaPreview1, mediaPreview2, mediaPreview3
};
Context context = mediaPreview0.getContext();
int mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(),
R.attr.media_preview_unloaded_drawable, android.R.color.black);
int mediaPreviewUnloadedId =
ThemeUtils.getDrawableId(itemView.getContext(), R.attr.media_preview_unloaded_drawable,
android.R.color.black);
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
@ -200,9 +238,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
previews[i].setVisibility(View.VISIBLE);
if (previewUrl == null || previewUrl.isEmpty()) {
Picasso.with(context)
.load(mediaPreviewUnloadedId)
.into(previews[i]);
Picasso.with(context).load(mediaPreviewUnloadedId).into(previews[i]);
} else {
Picasso.with(context)
.load(previewUrl)
@ -211,8 +247,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
final Status.MediaAttachment.Type type = attachments[i].type;
if (type == Status.MediaAttachment.Type.VIDEO
| type == Status.MediaAttachment.Type.GIFV) {
if (type == Status.MediaAttachment.Type.VIDEO | type == Status.MediaAttachment.Type.GIFV) {
videoIndicator.setVisibility(View.VISIBLE);
}
@ -233,7 +268,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (sensitive && (!isAlwayShowSensitive)) {
sensitiveMediaWarning.setVisibility(showingSensitive ? View.GONE : View.VISIBLE);
sensitiveMediaShow.setVisibility(showingSensitive ? View.VISIBLE : View.GONE);
sensitiveMediaShow.setOnClickListener(new View.OnClickListener(){
sensitiveMediaShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setVisibility(View.GONE);
@ -330,20 +365,19 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
contentWarningDescription.setText(spoilerText);
contentWarningBar.setVisibility(View.VISIBLE);
contentWarningButton.setChecked(expanded);
contentWarningButton.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(isChecked, getAdapterPosition());
}
if (isChecked) {
content.setVisibility(View.VISIBLE);
} else {
content.setVisibility(View.GONE);
}
}
});
contentWarningButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(isChecked, getAdapterPosition());
}
if (isChecked) {
content.setVisibility(View.VISIBLE);
} else {
content.setVisibility(View.GONE);
}
}
});
if (expanded) {
content.setVisibility(View.VISIBLE);
} else {
@ -441,7 +475,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setDisplayName(status.getUserFullName());
setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt());
setContent(status.getContent(), status.getMentions(), listener);
setContent(status.getContent(), status.getMentions(), status.getEmojis(), listener);
setAvatar(status.getAvatar(), status.getRebloggedAvatar());
setReblogged(status.isReblogged());
setFavourited(status.isFavourited());
@ -478,4 +512,58 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setSpoilerText(status.getSpoilerText(), status.isExpanded(), listener);
}
}
static class EmojiSpan extends ReplacementSpan implements Target {
private @Nullable
Drawable imageDrawable;
private WeakReference<Callback> callbackWeakReference;
public void setImageDrawable(@Nullable Drawable imageDrawable) {
this.imageDrawable = imageDrawable;
}
public void setCallback(Callback callback) {
this.callbackWeakReference = new WeakReference<Callback>(callback);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
@Nullable Paint.FontMetricsInt fm) {
if (imageDrawable == null) return 0;
Rect sizeRect = imageDrawable.getBounds();
return sizeRect.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
if (imageDrawable == null) return;
canvas.save();
int transY = bottom - imageDrawable.getBounds().bottom;
transY -= paint.getFontMetricsInt().descent;
canvas.translate(x, transY);
imageDrawable.draw(canvas);
canvas.restore();
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
imageDrawable = new BitmapDrawable(bitmap);
imageDrawable.setBounds(0, 0, imageDrawable.getIntrinsicWidth() + 10,
imageDrawable.getIntrinsicHeight() + 10);
if (callbackWeakReference != null) {
Callback cb = callbackWeakReference.get();
if (cb != null) cb.onSuccess();
}
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
}
}

View file

@ -24,6 +24,7 @@ import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import java.util.Date;
import java.util.List;
public class Status {
private Status actionableStatus;
@ -79,6 +80,8 @@ public class Status {
public boolean sensitive;
public List<Emoji> emojis;
@SerializedName("spoiler_text")
public String spoilerText;
@ -179,4 +182,17 @@ public class Status {
public String name;
public String website;
}
public static class Emoji {
private String shortcode;
private String url;
public String getShortcode() {
return shortcode;
}
public String getUrl() {
return url;
}
}
}

View file

@ -20,8 +20,7 @@ public final class ViewDataUtils {
public static StatusViewData statusToViewData(@Nullable Status status) {
if (status == null) return null;
Status visibleStatus = status.reblog == null ? status : status.reblog;
return new StatusViewData.Builder()
.setId(status.id)
return new StatusViewData.Builder().setId(status.id)
.setAttachments(visibleStatus.attachments)
.setAvatar(visibleStatus.account.avatar)
.setContent(visibleStatus.content)
@ -44,6 +43,7 @@ public final class ViewDataUtils {
.setSenderId(visibleStatus.account.id)
.setRebloggingEnabled(visibleStatus.rebloggingAllowed())
.setApplication(visibleStatus.application)
.setEmojis(visibleStatus.emojis)
.createStatusViewData();
}
@ -64,8 +64,8 @@ public final class ViewDataUtils {
statusToViewData(notification.status));
}
public static List<NotificationViewData>
notificationListToViewDataList(List<Notification> notifications) {
public static List<NotificationViewData> notificationListToViewDataList(
List<Notification> notifications) {
List<NotificationViewData> viewDatas = new ArrayList<>(notifications.size());
for (Notification n : notifications) {
viewDatas.add(notificationToViewData(n));

View file

@ -5,7 +5,9 @@ import android.text.Spanned;
import com.keylesspalace.tusky.entity.Status;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Created by charlag on 11/07/2017.
@ -41,16 +43,15 @@ public final class StatusViewData {
private final String senderId;
private final boolean rebloggingEnabled;
private final Status.Application application;
private final List<Status.Emoji> emojis;
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
String spoilerText, Status.Visibility visibility,
Status.MediaAttachment[] attachments, String rebloggedByUsername,
String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingSensitiveWarning, String userFullName, String nickname,
String avatar, Date createdAt, String reblogsCount,
String favouritesCount, String inReplyToId, Status.Mention[] mentions,
String senderId, boolean rebloggingEnabled,
Status.Application application) {
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
String rebloggedByUsername, String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId,
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Status.Emoji> emojis) {
this.id = id;
this.content = content;
this.reblogged = reblogged;
@ -74,6 +75,7 @@ public final class StatusViewData {
this.senderId = senderId;
this.rebloggingEnabled = rebloggingEnabled;
this.application = application;
this.emojis = emojis;
}
public String getId() {
@ -173,6 +175,10 @@ public final class StatusViewData {
return application;
}
public List<Status.Emoji> getEmojis() {
return emojis;
}
public static class Builder {
private String id;
private Spanned content;
@ -197,6 +203,7 @@ public final class StatusViewData {
private String senderId;
private boolean rebloggingEnabled;
private Status.Application application;
private List<Status.Emoji> emojis;
public Builder() {
}
@ -225,6 +232,7 @@ public final class StatusViewData {
senderId = viewData.senderId;
rebloggingEnabled = viewData.rebloggingEnabled;
application = viewData.application;
emojis = viewData.getEmojis();
}
public Builder setId(String id) {
@ -342,12 +350,17 @@ public final class StatusViewData {
return this;
}
public Builder setEmojis(List<Status.Emoji> emojis) {
this.emojis = emojis;
return this;
}
public StatusViewData createStatusViewData() {
if (this.emojis == null) emojis = Collections.emptyList();
return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility,
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt,
reblogsCount, favouritesCount, inReplyToId, mentions, senderId,
rebloggingEnabled, application);
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis);
}
}
}