Skip to content

Add support for emotes in flairs #1054

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/main/java/org/quantumbadger/redreader/common/BetterSSB.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
Expand All @@ -32,8 +33,9 @@
import androidx.annotation.NonNull;

import java.util.HashSet;
import java.util.Observable;

public class BetterSSB {
public class BetterSSB extends Observable {

private final SpannableStringBuilder sb;

Expand Down Expand Up @@ -178,6 +180,43 @@ public void linkify() {
}
}

public void append(final CharSequence text) {
this.sb.append(text);
this.setChanged();
this.notifyObservers(this.sb);
}

public void replace(final int start, final int end, final CharSequence text) {
this.sb.replace(start, end, text);
this.setChanged();
this.notifyObservers(this.sb);
}

public void replace(
@NonNull final CharSequence textToBeReplaced,
@NonNull final Object replacement) {
final int textStartIndex = TextUtils.indexOf(this.sb, textToBeReplaced);

this.sb.setSpan(replacement,
textStartIndex,
textStartIndex + textToBeReplaced.length(),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

this.setChanged();
this.notifyObservers(this.sb);
}

public boolean isEmpty() {
return this.sb.length() == 0;
}

public void setSpan(final Object what, final int start, final int end, final int flag) {
this.sb.setSpan(what, start, end, flag);

this.setChanged();
this.notifyObservers(this.sb);
}

public SpannableStringBuilder get() {
return sb;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.quantumbadger.redreader.common.LinkHandler
import org.quantumbadger.redreader.common.UriString
import org.quantumbadger.redreader.jsonwrap.JsonValue
import org.quantumbadger.redreader.reddit.things.RedditThingWithIdAndType
import org.quantumbadger.redreader.reddit.url.PostCommentListingURL

Expand All @@ -39,6 +40,7 @@ data class RedditComment(
val author: UrlEncodedString? = null,
val subreddit: UrlEncodedString? = null,
val author_flair_text: UrlEncodedString? = null,
val author_flair_richtext: List<MaybeParseError<FlairEmoteData>>?= null,
val archived: Boolean = false,
val likes: Boolean? = null,
val score_hidden: Boolean = false,
Expand Down Expand Up @@ -77,6 +79,33 @@ data class RedditComment(
// TODO do this in the HTML parser instead
fun copyWithNewBodyHtml(value: String) = copy(body_html = UrlEncodedString(value))

@Serializable
@Parcelize
data class EmoteMetadata(
val status: String,
val e: String,
val m: String,
val s: ImageMetadata,
val t: String,
val id: String
) : Parcelable

@Serializable
@Parcelize
data class ImageMetadata(
val x: String,
val y: String,
val u: String? = null
) : Parcelable

@Serializable
@Parcelize
data class FlairEmoteData(
val e: String,
val a: String,
val u: String
) : Parcelable

override fun getIdAlone() = id

override fun getIdAndType() = name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,53 @@

package org.quantumbadger.redreader.reddit.prepared;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.text.style.ImageSpan;
import android.util.Log;
import android.util.TypedValue;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import org.quantumbadger.redreader.account.RedditAccountManager;
import org.quantumbadger.redreader.cache.CacheManager;
import org.quantumbadger.redreader.cache.CacheRequest;
import org.quantumbadger.redreader.cache.CacheRequestCallbacks;
import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyIfNotCached;
import org.quantumbadger.redreader.common.BetterSSB;
import org.quantumbadger.redreader.common.Constants;
import org.quantumbadger.redreader.common.General;
import org.quantumbadger.redreader.common.GenericFactory;
import org.quantumbadger.redreader.common.PrefsUtility;
import org.quantumbadger.redreader.common.Priority;
import org.quantumbadger.redreader.common.RRError;
import org.quantumbadger.redreader.common.UriString;
import org.quantumbadger.redreader.common.datastream.SeekableInputStream;
import org.quantumbadger.redreader.common.time.TimestampUTC;
import org.quantumbadger.redreader.reddit.kthings.MaybeParseError;
import org.quantumbadger.redreader.reddit.kthings.RedditComment;
import org.quantumbadger.redreader.reddit.kthings.RedditIdAndType;
import org.quantumbadger.redreader.reddit.kthings.UrlEncodedString;
import org.quantumbadger.redreader.reddit.prepared.bodytext.BodyElement;
import org.quantumbadger.redreader.reddit.prepared.html.HtmlReader;
import org.quantumbadger.redreader.reddit.things.RedditThingWithIdAndType;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

public class RedditParsedComment implements RedditThingWithIdAndType {

private final RedditComment mSrc;

@NonNull private final BodyElement mBody;

private final BetterSSB mFlair;

public RedditParsedComment(
final RedditComment comment,
final AppCompatActivity activity) {
Expand All @@ -41,15 +73,30 @@ public RedditParsedComment(
mBody = HtmlReader.parse(
comment.getBody_html().getDecoded(), // TODO nullable?
activity);

final String flair = General.mapIfNotNull(
comment.getAuthor_flair_text(),
UrlEncodedString::getDecoded);

if(flair != null) {
mFlair = new BetterSSB();
mFlair.append(flair);

if (comment.getAuthor_flair_richtext() != null) {
getFlairEmotes(comment.getAuthor_flair_richtext(), activity);
}
} else {
mFlair = null;
}
}

@NonNull
public BodyElement getBody() {
return mBody;
}

public UrlEncodedString getFlair() {
return mSrc.getAuthor_flair_text();
public BetterSSB getFlair() {
return mFlair;
}

@Override
Expand All @@ -65,4 +112,116 @@ public RedditIdAndType getIdAndType() {
public RedditComment getRawComment() {
return mSrc;
}

private void getFlairEmotes(
final List<MaybeParseError<RedditComment.FlairEmoteData>> flairRichtext,
final AppCompatActivity activity) {

final int alignment;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
alignment = ImageSpan.ALIGN_CENTER;
} else {
alignment = ImageSpan.ALIGN_BASELINE;
}

for (final MaybeParseError<RedditComment.FlairEmoteData> flairEmoteData : flairRichtext) {
if (!(flairEmoteData instanceof MaybeParseError.Ok)) {
continue;
}

final RedditComment.FlairEmoteData flairEmoteObject =
((MaybeParseError.Ok< RedditComment.FlairEmoteData >) flairEmoteData)
.getValue();

@NonNull final String objectType = flairEmoteObject.getE();

if (objectType.equals("emoji")) {
final String placeholder = flairEmoteObject.getA();
final String url = flairEmoteObject.getU();

CacheManager.getInstance(activity).makeRequest(new CacheRequest(
new UriString(url),
RedditAccountManager.getAnon(),
null,
new Priority(Constants.Priority.API_COMMENT_LIST),
DownloadStrategyIfNotCached.INSTANCE,
Constants.FileType.IMAGE,
CacheRequest.DownloadQueueType.IMMEDIATE,
activity,
new CacheRequestCallbacks() {
Bitmap image = null;

@Override
public void onDataStreamComplete(
@NonNull final GenericFactory<SeekableInputStream, IOException>
stream,
final TimestampUTC timestamp,
@NonNull final UUID session,
final boolean fromCache,
@Nullable final String mimetype) {
try (InputStream is = stream.create()) {
image = BitmapFactory.decodeStream(is);

if (image == null) {
throw new IOException("Failed to decode bitmap");
}

final int textSize = 11;
final float maxImageHeightMultiple = 1.0F;

final float maxHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
PrefsUtility.appearance_fontscale_comment_headers()
* textSize
* maxImageHeightMultiple,
activity.getApplicationContext()
.getResources()
.getDisplayMetrics());

if (image.getHeight() > maxHeight) {
final float imageAspectRatio =
(float) image.getHeight() / image.getWidth();

final float newImageWidth = maxHeight / imageAspectRatio;

image = Bitmap.createScaledBitmap(image,
Math.round(newImageWidth),
Math.round(maxHeight),
true);
}

if (image == null) {
throw new IOException("Failed to decode bitmap");
}

final ImageSpan span = new ImageSpan(
activity.getApplicationContext(),
image,
alignment);

if (mFlair != null) {
mFlair.replace(placeholder, span);
}
} catch (final Throwable t) {
onFailure(new RRError(
"Exception while downloading emote",
null,
true,
t));
}
}

@Override
public void onFailure(@NonNull final RRError error) {
Log.e(
"RedditParsedComment",
"Failed to download emote: " + error.message,
error.t);
}
}
));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
Expand Down Expand Up @@ -53,7 +52,7 @@

public final class RedditPreparedMessage implements RedditRenderableInboxItem {

public final SpannableStringBuilder header;
public final BetterSSB header;
public final BodyElement body;
public final RedditIdAndType idAndType;
public final RedditMessage src;
Expand Down Expand Up @@ -141,10 +140,10 @@ public RedditPreparedMessage(
0,
1f);

header = sb.get();
header = sb;
}

public SpannableStringBuilder getHeader() {
public BetterSSB getHeader() {
return header;
}

Expand Down Expand Up @@ -183,7 +182,7 @@ public void handleInboxLongClick(final BaseActivity activity) {
}

@Override
public CharSequence getHeader(
public BetterSSB getHeader(
final RRThemeAttributes theme,
final RedditChangeDataManager changeDataManager,
final Context context,
Expand Down
Loading
Loading