Merge remote-tracking branch 'megalodon_main/main'

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
#	mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountByHandle.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowerListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/Account.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
#	mastodon/src/main/res/layout/display_item_footer.xml
#	mastodon/src/main/res/values/dimens.xml
This commit is contained in:
LucasGGamerM
2023-06-09 18:09:23 -03:00
81 changed files with 1549 additions and 819 deletions

View File

@@ -58,25 +58,24 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity;
private final HomeFragment fragment;
private final BiConsumer<String, Boolean> onClick;
private final boolean externalShare, openInApp;
private BiConsumer<String, Boolean> onClick;
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
private AccountsAdapter accountsAdapter;
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false, null);
this(activity, fragment, false, false);
}
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp, BiConsumer<String, Boolean> onClick){
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
super(activity);
this.activity=activity;
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);
@@ -122,6 +121,10 @@ public class AccountSwitcherSheet extends BottomSheet{
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(getContext(), R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
}
public void setOnClick(BiConsumer<String, Boolean> onClick) {
this.onClick = onClick;
}
private void confirmLogOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(activity)

View File

@@ -5,7 +5,6 @@ import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -20,7 +19,6 @@ import android.view.animation.BounceInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -59,8 +57,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private final TextView replies, boosts, favorites;
private final View reply, boost, favorite, share, bookmark;
private static final Animation opacityOut, opacityIn;
private static AnimationSet animSet;
@@ -105,22 +103,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);
reply=findViewById(R.id.reply);
boost=findViewById(R.id.boost);
favorite=findViewById(R.id.favorite);
bookmark=findViewById(R.id.bookmark);
share=findViewById(R.id.share);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
}
View reply=findViewById(R.id.reply_btn);
View boost=findViewById(R.id.boost_btn);
View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
replies=findViewById(R.id.reply);
boosts=findViewById(R.id.boost);
favorites=findViewById(R.id.favorite);
reply=findViewById(R.id.reply_btn);
boost=findViewById(R.id.boost_btn);
favorite=findViewById(R.id.favorite_btn);
share=findViewById(R.id.share_btn);
bookmark=findViewById(R.id.bookmark_btn);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
reply.setOnLongClickListener(this::onReplyLongClick);
@@ -145,9 +137,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(FooterStatusDisplayItem item){
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
bindText(replies, item.status.repliesCount);
bindText(boosts, item.status.reblogsCount);
bindText(favorites, item.status.favouritesCount);
// in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
@@ -166,12 +158,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
condenseBottom ? V.dp(-5) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){
private void bindText(TextView btn, long count){
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(8));
@@ -195,8 +187,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
// 28dp to center in middle of icon, because:
// (icon width = 24dp) / 2 + (paddingStart = 8dp) + (paddingHorizontal = 8dp)
v.setPivotX(UiUtils.sp(v.getContext(), 28));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
@@ -265,7 +258,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
bindText(boosts, r.reblogsCount);
}
private boolean onBoostLongClick(View v){

View File

@@ -202,7 +202,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putBoolean("navigateToStatus", true);
}
}
if(!redraft && TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
boolean isPixelfed = item.parentFragment.isInstancePixelfed();
boolean textEmpty = TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText);
if(!redraft && (isPixelfed || textEmpty)){
// pixelfed doesn't support /statuses/:id/source :/
if (isPixelfed) {
args.putString("sourceText", HtmlParser.text(item.status.content));
args.putString("sourceSpoiler", item.status.spoilerText);
}
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);

View File

@@ -73,9 +73,9 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
default -> 0;
} : 0;
}

View File

@@ -129,7 +129,7 @@ public abstract class StatusDisplayItem{
: fragment.getString(R.string.in_reply_to, account.displayName);
replyLine = new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20_filled, null, null, fullText
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
}
@@ -138,7 +138,7 @@ public abstract class StatusDisplayItem{
statusForContent.rebloggedBy = status.account;
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, fullText));
@@ -153,7 +153,7 @@ public abstract class StatusDisplayItem{
// post contains a hashtag the user is following
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, hashtag.name, List.of(),
R.drawable.ic_fluent_number_symbol_20_filled, null,
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
i -> {
args.putString("hashtag", hashtag.name);
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);

View File

@@ -65,7 +65,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
UiUtils.loadMaxWidth(parentFragment.getContext());
}
public void setTranslationShown(boolean translationShown) {
@@ -227,13 +226,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int pos = getAbsoluteAdapterPosition();
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
(translateVisible &&
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
? 0 : V.dp(12)
);
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
int bottomPadding = (translateVisible && nextIsFooter) ? 0
: nextIsFooter ? V.dp(6)
: V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams);

View File

@@ -4,128 +4,121 @@ import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class ClickableLinksDelegate {
private Paint hlPaint;
private final Paint hlPaint;
private Path hlPath;
private LinkSpan selectedSpan;
private TextView view;
private final Runnable longClickRunnable = () -> {
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
private final TextView view;
private final GestureDetector gestureDetector;
public ClickableLinksDelegate(TextView view) {
this.view=view;
hlPaint=new Paint();
hlPaint.setAntiAlias(true);
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
hlPaint.setStrokeWidth(V.dp(4));
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
}
public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime();
if(event.getAction()==MotionEvent.ACTION_DOWN){
int line=-1;
Rect rect=new Rect();
Layout l=view.getLayout();
for(int i=0;i<l.getLineCount();i++){
view.getLineBounds(i, rect);
if(rect.contains((int)event.getX(), (int)event.getY())){
line=i;
break;
}
}
if(line==-1){
return false;
}
CharSequence text=view.getText();
if(text instanceof Spanned){
Spanned s=(Spanned)text;
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
int lstart=l.getLineForOffset(start);
int lend=l.getLineForOffset(end);
if(line>=lstart && line<=lend){
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
continue;
}
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
continue;
}
hlPath=new Path();
selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
Rect bounds=new Rect();
l.getLineBounds(j, bounds);
//bounds.left+=view.getPaddingLeft();
if(j==lstart){
bounds.left=Math.round(l.getPrimaryHorizontal(start));
}
if(j==lend){
bounds.right=Math.round(l.getPrimaryHorizontal(end));
}else{
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
}
bounds.inset(V.dp(-2), V.dp(-2));
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
}
hlPath.offset(view.getPaddingLeft(), 0);
view.invalidate();
return true;
}
}
}
}
}
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
}
view.removeCallbacks(longClickRunnable);
hlPath=null;
selectedSpan=null;
view.invalidate();
return false;
}
if(event.getAction()==MotionEvent.ACTION_CANCEL){
hlPath=null;
selectedSpan=null;
view.removeCallbacks(longClickRunnable);
view.invalidate();
return false;
// the gestureDetector does not provide a callback for CANCEL, therefore:
// remove background color of view before passing event to gestureDetector
resetAndInvalidate();
}
return false;
return gestureDetector.onTouchEvent(event);
}
/**
* remove highlighting from span and let the system redraw the view
*/
private void resetAndInvalidate() {
hlPath=null;
selectedSpan=null;
view.invalidate();
}
public void onDraw(Canvas canvas){
if(hlPath!=null){
canvas.save();
canvas.translate(0, view.getPaddingTop());
canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
canvas.drawPath(hlPath, hlPaint);
canvas.restore();
}
}
/**
* GestureListener for spans that represent URLs.
* onDown: on start of touch event, set highlighting
* onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting
* onLongPress: copy URL to clipboard, let user know, reset highlighting
*/
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent event) {
int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
float x=event.getX(), y=event.getY();
if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
return false;
x-=padLeft;
y-=padTop;
Layout l=view.getLayout();
int line=l.getLineForVertical(Math.round(y));
int position=l.getOffsetForHorizontal(line, x);
CharSequence text=view.getText();
if(text instanceof Spanned s){
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
if(start<=position && end>position){
selectedSpan=span;
hlPath=new Path();
l.getSelectionPath(start, end, hlPath);
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
view.invalidate();
return true;
}
}
}
return super.onDown(event);
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent event) {
if(selectedSpan!=null){
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
resetAndInvalidate();
return true;
}
return false;
}
@Override
public void onLongPress(@NonNull MotionEvent event) {
if (selectedSpan == null) return;
UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText());
//reset view
resetAndInvalidate();
}
}
}

View File

@@ -1,14 +1,10 @@
package org.joinmastodon.android.ui.text;
import android.graphics.Typeface;
import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontStyle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
@@ -17,13 +13,10 @@ import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.util.TypedValue;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Mention;
@@ -251,6 +244,10 @@ public class HtmlParser{
return Jsoup.clean(html, Safelist.none());
}
public static String text(String html) {
return Jsoup.parse(html).body().wholeText();
}
public static CharSequence parseLinks(String text){
Matcher matcher=URL_PATTERN.matcher(text);
if(!matcher.find()) // Return the original string if there are no URLs

View File

@@ -17,6 +17,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -37,6 +38,7 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
@@ -56,6 +58,8 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
@@ -101,10 +105,9 @@ import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -147,15 +150,11 @@ public class UiUtils {
private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public static int MAX_WIDTH;
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
private UiUtils() {
}
public static void loadMaxWidth(Context ctx) {
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
}
public static void launchWebBrowser(Context context, String url) {
try {
if (GlobalUserPreferences.useCustomTabs) {
@@ -197,19 +196,11 @@ public class UiUtils {
}
}
public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=now-t;
if(diff<1000L){
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
long diff = now - t;
if (diff < 1000L) {
return context.getString(R.string.time_just_now);
} else if (diff < 60_000L) {
int secs = (int) (diff / 1000L);
@@ -285,7 +276,7 @@ public class UiUtils {
mainHandler.post(runnable);
}
public static void runOnUiThread(Runnable runnable, long delay){
public static void runOnUiThread(Runnable runnable, long delay) {
mainHandler.postDelayed(runnable, delay);
}
@@ -538,15 +529,15 @@ public class UiUtils {
new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
public void onSuccess(Relationship result) {
resultCallback.accept(result);
if(!currentlyMuted){
if (!currentlyMuted) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@@ -557,27 +548,28 @@ public class UiUtils {
.setIcon(currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular)
.show();
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback) {
confirmDeletePost(activity, accountID, status, resultCallback, false);
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft) {
showConfirmationAlert(activity,
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result){
public void onSuccess(Status result) {
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@@ -586,7 +578,7 @@ public class UiUtils {
);
}
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback) {
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
showConfirmationAlert(activity,
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
@@ -602,7 +594,7 @@ public class UiUtils {
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@@ -611,13 +603,13 @@ public class UiUtils {
);
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback) {
showConfirmationAlert(activity,
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular,
()->{
() -> {
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@Override
@@ -680,7 +672,7 @@ public class UiUtils {
.exec(accountID));
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
public static void setRelationshipToActionButton(Relationship relationship, Button button) {
setRelationshipToActionButton(relationship, button, false);
}
@@ -784,17 +776,8 @@ public class UiUtils {
if (relationship.blocking) {
confirmToggleBlockUser(activity, accountID, account, true, resultCallback);
return;
}
if(relationship.muting){
} else if (relationship.muting) {
confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
return;
}
progressCallback.accept(true);
if (!relationship.following && !relationship.requested) {
follow(activity, accountID, account, true, progressCallback, resultCallback);
} else {
showConfirmationAlert(activity,
activity.getString(R.string.mo_confirm_unfollow_title),
@@ -819,13 +802,14 @@ public class UiUtils {
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
progressCallback.accept(false);
}
})
.exec(accountID);
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
progressCallback.accept(false);
}
})
.exec(accountID);
}
}
@@ -932,8 +916,6 @@ public class UiUtils {
}
}
/// Add icons to the menu.
/// Passing in items will be colored to be visible on the background.
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try {
@@ -941,8 +923,8 @@ public class UiUtils {
m.setAccessible(true);
m.invoke(menu, true);
enableMenuIcons(context, menu, asAction);
} catch (Exception ignored) {
}
catch(Exception ignored){}
}
}
@@ -982,6 +964,10 @@ public class UiUtils {
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
Resources res = context.getResources();
MAX_WIDTH = (int) res.getDimension(R.dimen.layout_max_width);
SCROLL_TO_TOP_DELTA = (int) res.getDimension(R.dimen.scroll_to_top_delta);
}
public static boolean isDarkTheme() {
@@ -990,6 +976,32 @@ public class UiUtils {
return theme == GlobalUserPreferences.ThemePreference.DARK;
}
public static Optional<Pair<String, Optional<String>>> parseFediverseHandle(String maybeFediHandle) {
// https://stackoverflow.com/a/26987741, except i put a + here ... v
String domainRegex = "^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]\\.)+(xn--)?([a-z0-9][a-z0-9\\-]{0,60}|[a-z0-9-]{1,30}\\.[a-z]{2,})$";
if (maybeFediHandle.toLowerCase().startsWith("mailto:")) {
maybeFediHandle = maybeFediHandle.substring("mailto:".length());
}
List<String> parts = Arrays.stream(maybeFediHandle.split("@"))
.filter(part -> !part.isEmpty())
.collect(Collectors.toList());
if (parts.size() == 0 || !parts.get(0).matches("^[^/\\s]+$")) {
return Optional.empty();
} else if (parts.size() == 2) {
try {
String domain = IDN.toASCII(parts.get(1));
if (!domain.matches(domainRegex)) return Optional.empty();
return Optional.of(Pair.create(parts.get(0), Optional.of(parts.get(1))));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
} else if (maybeFediHandle.startsWith("@")) {
return Optional.of(Pair.create(parts.get(0), Optional.empty()));
} else {
return Optional.empty();
}
}
// https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User
@@ -1110,150 +1122,48 @@ public class UiUtils {
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
);
}
public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
);
}
public static void lookupRemoteStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
remoteLookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
);
}
public static void lookupRemoteAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
remoteLookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
);
}
public static <T extends Searchable> void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
public static <T extends Searchable> Optional<MastodonAPIRequest<SearchResults>> lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
resultConsumer.accept(query);
return;
return Optional.empty();
}
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
.exec(targetAccountID));
}
public static <T extends Searchable> void remoteLookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
resultConsumer.accept(query);
return;
}
Pattern pattern = Pattern.compile("(?<=\\/\\/)([^\\/]+)(?=\\/@)");
Matcher matcher = pattern.matcher(query.getQuery());
String domain = null;
if(matcher.find()){
domain = matcher.group(1);
}
// if(domain == null){
// return;
// }
Pattern patternForQuery = Pattern.compile("https?:\\/\\/[^\\/]+\\/@(\\w+)");
Matcher matcherForQuery = patternForQuery.matcher(query.getQuery());
String trimmedQuery = null;
if(matcherForQuery.find()){
trimmedQuery = matcherForQuery.group(1);
}
// if(trimmedQuery == null){
// return;
// }
if(query instanceof Account){
domain = ((Account) query).getDomain();
trimmedQuery = ((Account) query).username;
}
String finalDomain = domain;
if(query instanceof Account){
new GetAccountByHandle(((Account) query).acct)
.setCallback(new Callback<Account>() {
@Override
public void onSuccess(Account result) {
if(result != null){
resultConsumer.accept((T) result);
} else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
resultConsumer.accept(null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d, finalDomain))
.execNoAuth(domain);
return;
}
new GetSearchResults(trimmedQuery, type, false).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
resultConsumer.accept(null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d, finalDomain))
.execNoAuth(domain);
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog){
transformDialogForLookup(context, accountID, url, dialog, null);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog, @Nullable String instanceName) {
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, instanceName != null ? instanceName : getInstanceName(accountID)));
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
dialog.setTitle(R.string.sk_loading_fediverse_resource_title);
}
@@ -1266,19 +1176,97 @@ public class UiUtils {
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> {
if (clazz == null) return;
Nav.go((Activity) context, clazz, args);
});
private static Bundle bundleError(String error) {
Bundle args = new Bundle();
args.putString("error", error);
return args;
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
private static Bundle bundleError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
return args;
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, (clazz, args) -> {
if (clazz == null) {
if (args != null && args.containsKey("error")) Toast.makeText(context, args.getString("error"), Toast.LENGTH_SHORT).show();
if (launchBrowser) launchWebBrowser(context, url);
return;
}
Nav.go((Activity) context, clazz, args);
}).map(req -> req.wrapProgress((Activity) context, R.string.loading, true, d ->
transformDialogForLookup(context, accountID, url, d)));
}
public static boolean acctMatches(String accountID, String acct, String queriedUsername, @Nullable String queriedDomain) {
// check if the username matches
if (!acct.split("@")[0].equalsIgnoreCase(queriedUsername)) return false;
boolean resultOnHomeInstance = !acct.contains("@");
if (resultOnHomeInstance) {
// acct is formatted like 'someone'
// only allow home instance result if query didn't specify a domain,
// or the specified domain does, in fact, match the account session's domain
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
return queriedDomain == null || session.domain.equalsIgnoreCase(queriedDomain);
} else if (queriedDomain == null) {
// accept whatever result we have as there's no queried domain to compare to
return true;
} else {
// acct is formatted like 'someone@somewhere'
return acct.split("@")[1].equalsIgnoreCase(queriedDomain);
}
}
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
return parseFediverseHandle(query).map(
handle -> lookupAccountHandle(context, accountID, handle, go))
.or(() -> {
go.accept(null, null);
return Optional.empty();
});
}
public static MastodonAPIRequest<SearchResults> lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Bundle args = new Bundle();
args.putString("account", accountID);
Optional<Account> account = results.accounts.stream()
.filter(a -> acctMatches(accountID, a.acct, queryHandle.first, queryHandle.second.orElse(null)))
.findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
go.accept(ProfileFragment.class, args);
return;
}
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
go.accept(null, bundleError(error));
}
}).exec(accountID);
}
public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
Uri uri = Uri.parse(url);
List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1))
return Optional.of(new GetStatusByID(path.get(1))
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
@@ -1290,17 +1278,12 @@ public class UiUtils {
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeMastodonUrl(url)) {
new GetSearchResults(url, null, true)
.exec(accountID));
} else if (looksLikeFediverseUrl(url)) {
return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1318,26 +1301,19 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
}
}
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
return Optional.empty();
}
public static void copyText(View v, String text) {
@@ -1367,6 +1343,14 @@ public class UiUtils {
return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"));
}
public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText) {
Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText);
@@ -1389,15 +1373,6 @@ public class UiUtils {
}
}
// public static String getVisibilityText(Status status) {
// return MastodonApp.context.getString(switch (status.visibility) {
// case PUBLIC -> R.string.visibility_public;
// case UNLISTED -> R.string.sk_visibility_unlisted;
// case PRIVATE -> R.string.visibility_followers_only;
// case DIRECT -> R.string.visibility_private;;
// });
// }
// https://github.com/tuskyapp/Tusky/pull/3148
public static void reduceSwipeSensitivity(ViewPager2 pager) {
try {
@@ -1501,6 +1476,33 @@ public class UiUtils {
});
}
public static void showFragmentForNotification(Context context, Notification n, String accountID, Bundle extras) {
if (extras == null) extras = new Bundle();
extras.putString("account", accountID);
if (n.status!=null) {
Status status=n.status;
extras.putParcelable("status", Parcels.wrap(status.clone()));
Nav.go((Activity) context, ThreadFragment.class, extras);
} else if (n.report != null) {
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
UiUtils.launchWebBrowser(context, "https://"+domain+"/admin/reports/"+n.report.id);
} else if (n.account != null) {
extras.putString("account", accountID);
extras.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go((Activity) context, ProfileFragment.class, extras);
}
}
/**
* Scale the input value according to the device's scaled display density
* @param sp Input value in scale-independent pixels (sp)
* @return Scaled value in physical pixels (px)
*/
public static int sp(Context context, float sp){
// TODO: replace with V.sp in next AppKit version
return Math.round(sp*context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity);
}
/**
* Wraps a View.OnClickListener to filter multiple clicks in succession.
* Useful for buttons that perform some action that changes their state asynchronously.

View File

@@ -23,7 +23,6 @@ public class ComposeMediaLayout extends ViewGroup{
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override

View File

@@ -3,12 +3,13 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.joinmastodon.android.R;
public class MaxWidthFrameLayout extends FrameLayout{
private int maxWidth;
private int maxWidth, defaultWidth;
public MaxWidthFrameLayout(Context context){
this(context, null);
@@ -22,6 +23,7 @@ public class MaxWidthFrameLayout extends FrameLayout{
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
defaultWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_defaultWidth, -1);
ta.recycle();
}
@@ -33,10 +35,19 @@ public class MaxWidthFrameLayout extends FrameLayout{
this.maxWidth=maxWidth;
}
public int getDefaultWidth() {
return defaultWidth;
}
public void setDefaultWidth(int defaultWidth) {
this.defaultWidth = defaultWidth;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
widthMeasureSpec=maxWidth | MeasureSpec.getMode(widthMeasureSpec);
int width = defaultWidth >= 0 ? defaultWidth : maxWidth;
widthMeasureSpec=width | MeasureSpec.getMode(widthMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

View File

@@ -27,7 +27,6 @@ public class MediaGridLayout extends ViewGroup{
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
UiUtils.loadMaxWidth(context);
}
@Override