Compare commits

...

14 Commits

Author SHA1 Message Date
Grishka
1ad2d08e27 Merge branch 'l10n_master' 2024-10-10 04:09:47 +03:00
Grishka
42658add38 Fix opening links in non-browser apps 2024-10-10 04:09:20 +03:00
Grishka
b211789847 Fix colors for quotes and code blocks 2024-10-10 01:29:21 +03:00
Eugen Rochko
9c88183366 New translations strings.xml (Russian) 2024-10-09 20:16:06 +02:00
Eugen Rochko
c76dba3a8c New translations strings.xml (Basque) 2024-10-09 18:07:53 +02:00
Eugen Rochko
29bee87f2a New translations strings.xml (Basque) 2024-10-09 16:54:04 +02:00
Grishka
c139f85b99 Fix wrong unread notification count for some accounts on 4.3 2024-10-09 06:09:53 +03:00
Grishka
3247d4f2f5 Fix notifications loading on pre-4.3 servers
fixes #897
2024-10-09 05:28:01 +03:00
Grishka
77b2f98f17 Quotes in text formatting (AND-222) 2024-10-09 05:20:58 +03:00
Grishka
82c6c8076a Lists in text formatting (AND-221) 2024-10-09 03:00:37 +03:00
Grishka
4177faa553 Monospace text formatting (AND-223) 2024-10-09 01:21:50 +03:00
Eugen Rochko
92ec125661 New translations strings.xml (Indonesian) 2024-10-08 12:59:00 +02:00
Grishka
513a57663b Display bold, italic, and strikethrough formatting (AND-220, AND-224) 2024-10-07 18:48:06 +03:00
Eugen Rochko
20e7f716f1 New translations strings.xml (Chinese Simplified) 2024-10-07 08:00:50 +02:00
41 changed files with 442 additions and 43 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 34
versionCode 121
versionName "2.7.2"
versionCode 123
versionName "2.7.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -90,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.4.2'
implementation 'me.grishka.appkit:appkit:1.4.3'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -23,7 +23,11 @@
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
<data android:scheme="http" android:host="*"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="*"/>
</intent>
</queries>

View File

@@ -295,6 +295,15 @@ public class CacheController{
.collect(Collectors.toList());
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(converted, result.isEmpty() ? null : result.get(result.size()-1).id);
callback.onSuccess(res);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
databaseThread.postRunnable(()->putNotifications(converted.stream().map(nvm->nvm.notification).collect(Collectors.toList()), accounts, statuses, onlyMentions, maxID==null), 0);
}

View File

@@ -1,10 +1,27 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationType;
import java.util.EnumSet;
public class GetUnreadNotificationsCount extends MastodonAPIRequest<GetUnreadNotificationsCount.Response>{
public GetUnreadNotificationsCount(){
public GetUnreadNotificationsCount(EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
super(HttpMethod.GET, "/notifications/unread_count", Response.class);
if(includeTypes!=null){
for(String type: ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
addQueryParameter("types[]", type);
}
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
addQueryParameter("exclude_types[]", type);
}
}
if(groupedTypes!=null){
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
addQueryParameter("grouped_types[]", type);
}
}
}
@Override

View File

@@ -84,7 +84,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
public void onSuccess(HeaderPaginationList<Account> result){
for(Account acc:result)
accountIDsInList.add(acc.id);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()));
}
})
.exec(accountID);

View File

@@ -296,7 +296,7 @@ public class HomeFragment extends AppKitFragment{
if(instance==null)
return;
if(instance.getApiVersion()>=2){
new GetUnreadNotificationsCount()
new GetUnreadNotificationsCount(EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetUnreadNotificationsCount.Response result){

View File

@@ -190,7 +190,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
@Subscribe
public void onAccountAddedToList(AccountAddedToListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
data.add(new AccountViewModel(ev.account, accountID));
data.add(new AccountViewModel(ev.account, accountID, getActivity()));
list.getAdapter().notifyItemInserted(data.size()-1);
}
}
@@ -337,7 +337,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
onDone.run();
for(Account acc:accounts){
accountIDsInList.add(acc.id);
data.add(new AccountViewModel(acc, accountID));
data.add(new AccountViewModel(acc, accountID, getActivity()));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
}

View File

@@ -80,7 +80,7 @@ public class NotificationRequestsFragment extends MastodonRecyclerFragment<Notif
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false, getActivity()));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);

View File

@@ -580,7 +580,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
}else{
@@ -615,7 +615,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
fields.add(joined);
for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);

View File

@@ -61,7 +61,7 @@ public class AccountSearchFragment extends BaseAccountListFragment{
protected void onSuccess(List<Account> result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
}
protected String getSearchViewPlaceholder(){

View File

@@ -44,7 +44,7 @@ public class AddNewListMembersFragment extends AccountSearchFragment{
@Override
public void onSuccess(HeaderPaginationList<Account> result){
setEmptyText("");
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), result.nextPageUri!=null);
maxID=result.getNextPageMaxID();
}
})

View File

@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);

View File

@@ -31,7 +31,7 @@ public class DiscoverAccountsFragment extends BaseAccountListFragment implements
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity())).collect(Collectors.toList());
onDataLoaded(accounts, false);
bannerHelper.onBannerBecameVisible();
}

View File

@@ -109,7 +109,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true, getActivity());
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr));
}
@@ -126,7 +126,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false, getActivity());
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr));
}

View File

@@ -70,7 +70,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity()).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);

View File

@@ -128,7 +128,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
hlp.leftMargin=hlp.rightMargin=V.dp(16);
scrollingLayout.addView(heading, hlp);
AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID);
AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID, getActivity());
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
holder.bind(model);

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model.viewmodel;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
@@ -24,11 +25,11 @@ public class AccountViewModel{
public final CharSequence parsedName, parsedBio;
public final String verifiedLink;
public AccountViewModel(Account account, String accountID){
this(account, accountID, true);
public AccountViewModel(Account account, String accountID, Context context){
this(account, accountID, true, context);
}
public AccountViewModel(Account account, String accountID, boolean needBio){
public AccountViewModel(Account account, String accountID, boolean needBio, Context context){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
@@ -38,7 +39,7 @@ public class AccountViewModel{
parsedName=account.displayName;
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
if(needBio){
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, context);
ssb.append(parsedBio);
}else{
parsedBio=null;

View File

@@ -11,10 +11,10 @@ public class SearchResultViewModel{
public AccountViewModel account;
public ListItem<Hashtag> hashtagItem;
public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents){
public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents, Context context){
this.result=result;
switch(result.type){
case ACCOUNT -> account=new AccountViewModel(result.account, accountID);
case ACCOUNT -> account=new AccountViewModel(result.account, accountID, context);
case HASHTAG -> {
hashtagItem=new ListItem<>((isRecents ? "#" : "")+result.hashtag.name, null, isRecents ? R.drawable.ic_history_24px : R.drawable.ic_tag_24px, null, result.hashtag);
hashtagItem.isEnabled=true;

View File

@@ -15,7 +15,7 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
super(parentID, parentFragment);
this.account=new AccountViewModel(account, parentFragment.getAccountID());
this.account=new AccountViewModel(account, parentFragment.getAccountID(), parentFragment.getActivity());
}
@Override

View File

@@ -39,7 +39,7 @@ public class InlineStatusStatusDisplayItem extends StatusDisplayItem{
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(parsedName, status.account.emojis);
parsedPostText=HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, parentFragment.getAccountID(), status.getContentStatus());
parsedPostText=HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, parentFragment.getAccountID(), status.getContentStatus(), parentFragment.getActivity());
for(Object span:parsedPostText.getSpans(0, parsedPostText.length(), Object.class)){
if(!(span instanceof CustomEmojiSpan))
parsedPostText.removeSpan(span);

View File

@@ -141,7 +141,7 @@ public abstract class StatusDisplayItem{
}
if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, statusForContent);
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, statusForContent, fragment.getActivity());
if(filtered){
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
}

View File

@@ -58,7 +58,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void setTranslatedText(String text){
Status statusForContent=status.getContentStatus();
translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID(), statusForContent);
translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID(), statusForContent, parentFragment.getActivity());
translationEmojiHelper.setText(translatedText);
}

View File

@@ -0,0 +1,35 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.TypefaceSpan;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public abstract class BaseMonospaceSpan extends TypefaceSpan{
private final Context context;
public BaseMonospaceSpan(Context context){
super("monospace");
this.context=context;
}
@Override
public void updateDrawState(@NonNull TextPaint paint){
super.updateDrawState(paint);
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextText));
paint.setTextSize(paint.getTextSize()*0.9375f);
paint.baselineShift=V.dp(-1);
}
@Override
public void updateMeasureState(@NonNull TextPaint paint){
super.updateMeasureState(paint);
paint.setTextSize(paint.getTextSize()*0.9375f);
paint.baselineShift=V.dp(-1);
}
}

View File

@@ -0,0 +1,83 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.LeadingMarginSpan;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public class BlockQuoteSpan extends CharacterStyle implements LeadingMarginSpan{
private final Context context;
private Drawable icon;
private boolean firstLevel;
private Paint paint=new Paint();
public BlockQuoteSpan(Context context, boolean firstLevel){
this.context=context;
icon=context.getResources().getDrawable(R.drawable.quote, context.getTheme()).mutate();
this.firstLevel=firstLevel;
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(3));
}
@Override
public int getLeadingMargin(boolean first){
return V.dp(firstLevel ? 32 : 18);
}
@Override
public void drawLeadingMargin(@NonNull Canvas c, @NonNull Paint p, int x, int dir, int top, int baseline, int bottom, @NonNull CharSequence text, int start, int end, boolean first, @NonNull Layout layout){
if(text instanceof Spanned s && s.getSpanStart(this)==start){
int color;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S && UiUtils.isDarkTheme()){
color=UiUtils.alphaBlendColors(
context.getColor(android.R.color.system_accent3_700),
context.getColor(android.R.color.system_accent3_800),
0.5f
);
}else{
color=UiUtils.getThemeColor(context, R.attr.colorRichTextDecorations);
}
int level=s.getSpans(start, end, LeadingMarginSpan.class).length-1;
if(dir<0){ // RTL
if(level==0){
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setBounds(layout.getWidth()-icon.getIntrinsicWidth(), top, layout.getWidth(), top+icon.getIntrinsicHeight());
icon.draw(c);
}else{
paint.setColor(color);
float xOffset=layout.getWidth()-V.dp(32+18*(level-1)+1.5f);
c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint);
}
}else{
if(level==0){
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setBounds(x, top, x+icon.getIntrinsicWidth(), top+icon.getIntrinsicHeight());
icon.draw(c);
}else{
paint.setColor(color);
float xOffset=x+V.dp(32+18*(level-1)+1.5f);
c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint);
}
}
}
}
@Override
public void updateDrawState(TextPaint tp){
tp.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextText));
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
public class CodeBlockSpan extends BaseMonospaceSpan{
public CodeBlockSpan(Context context){
super(context);
}
}

View File

@@ -1,11 +1,17 @@
package org.joinmastodon.android.ui.text;
import android.annotation.SuppressLint;
import android.content.Context;
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.LineHeightSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
@@ -34,6 +40,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public class HtmlParser{
private static final String TAG="HtmlParser";
@@ -69,7 +76,7 @@ public class HtmlParser{
* @param emojis Custom emojis that are present in source as <code>:code:</code>
* @return a spanned string
*/
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject){
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject, Context context){
class SpanInfo{
public Object span;
public int start;
@@ -92,10 +99,34 @@ public class HtmlParser{
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
private boolean isInsidePre(){
for(SpanInfo si:openSpans){
if(si.span instanceof CodeBlockSpan)
return true;
}
return false;
}
private boolean isInsideBlockquote(){
for(SpanInfo si:openSpans){
if(si.span instanceof BlockQuoteSpan)
return true;
}
return false;
}
@SuppressLint("DefaultLocale")
@Override
public void head(@NonNull Node node, int depth){
if(node instanceof TextNode textNode){
ssb.append(textNode.text());
if(isInsidePre()){
ssb.append(textNode.getWholeText().stripTrailing());
}else{
String text=textNode.text();
if(ssb.length()==0 || ssb.charAt(ssb.length()-1)=='\n')
text=text.stripLeading();
ssb.append(text);
}
}else if(node instanceof Element el){
switch(el.nodeName()){
case "a" -> {
@@ -131,6 +162,44 @@ public class HtmlParser{
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
}
}
case "b", "strong" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
case "i", "em" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el));
case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el));
case "code" -> {
if(!isInsidePre()){
openSpans.add(new SpanInfo(new MonospaceSpan(context), ssb.length(), el));
ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
case "pre" -> openSpans.add(new SpanInfo(new CodeBlockSpan(context), ssb.length(), el));
case "li" -> {
Element parent=el.parent();
if(parent==null)
return;
if(ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n')
ssb.append('\n');
String markerText;
if("ol".equals(parent.nodeName())){
markerText=String.format("%d.", (parent.hasAttr("start") ? safeParseInt(parent.attr("start")) : 1)+el.elementSiblingIndex());
}else{
markerText="";
}
openSpans.add(new SpanInfo(new ListItemMarkerSpan(markerText), ssb.length(), el));
StringBuilder copyableText=new StringBuilder();
for(SpanInfo si:openSpans){
if(si.span instanceof ListItemMarkerSpan ims){
copyableText.append(ims.text);
}
}
copyableText.append(' ');
ssb.append(copyableText.toString(), new InvisibleSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
case "blockquote" -> {
if(ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n')
ssb.append('\n');
openSpans.add(new SpanInfo(new BlockQuoteSpan(context, !isInsideBlockquote()), ssb.length(), el));
}
}
}
}
@@ -138,15 +207,27 @@ public class HtmlParser{
@Override
public void tail(@NonNull Node node, int depth){
if(node instanceof Element el){
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
String name=el.nodeName();
if("span".equals(name) && el.hasClass("ellipsis")){
ssb.append("", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}else if("p".equals(el.nodeName())){
}else if("p".equals(name) || "ol".equals(name) || "ul".equals(name)){
if(node.nextSibling()!=null && "body".equals(node.parent().nodeName())){
ssb.append('\n');
ssb.append("\n", new SpacerSpan(1, V.dp(8)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}else if("pre".equals(name)){
if(node.nextSibling()!=null)
ssb.append("\n\n");
}else if(!openSpans.isEmpty()){
ssb.append("\n");
}
if(!openSpans.isEmpty()){
SpanInfo si=openSpans.get(openSpans.size()-1);
if(si.element==el){
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if(si.span!=null){
if(si.span instanceof MonospaceSpan){
ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
openSpans.remove(openSpans.size()-1);
}
}
@@ -158,6 +239,14 @@ public class HtmlParser{
return ssb;
}
private static int safeParseInt(String s){
try{
return Integer.parseInt(s);
}catch(NumberFormatException x){
return 0;
}
}
public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){
Map<String, Emoji> emojiByCode =
emojis.stream()

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.ui.text;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import me.grishka.appkit.utils.V;
public class ListItemMarkerSpan implements LeadingMarginSpan{
public String text;
public ListItemMarkerSpan(String text){
this.text=text;
}
@Override
public int getLeadingMargin(boolean first){
return V.dp(32);
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout){
if(text instanceof Spanned s && s.getSpanStart(this)==start){
int level=s.getSpans(start, end, LeadingMarginSpan.class).length-1;
if(dir<0){ // RTL
c.drawText(this.text, layout.getWidth()-V.dp(32*level)-p.measureText(this.text), baseline, p);
}else{
c.drawText(this.text, x+V.dp(32*level), baseline, p);
}
}
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
public class MonospaceSpan extends BaseMonospaceSpan{
public MonospaceSpan(Context context){
super(context);
}
}

View File

@@ -17,7 +17,12 @@ public class SpacerSpan extends ReplacementSpan{
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
// TODO height
if(fm!=null && height>0){
fm.ascent=-height;
fm.descent=0;
fm.top=fm.ascent;
fm.bottom=0;
}
return width;
}

View File

@@ -713,8 +713,8 @@ public class UiUtils{
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
ssb.setSpan(new SpacerSpan(V.dp(24), 0), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 0), 0);
item.setTitle(ssb);
}
}

View File

@@ -244,7 +244,7 @@ public class ComposeAutocompleteViewController{
if(mode!=Mode.USERS)
return;
List<AccountViewModel> oldList=users;
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList());
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID, activity)).collect(Collectors.toList());
if(isLoading){
isLoading=false;
if(users.size()>=LOADING_FAKE_USER_COUNT){

View File

@@ -4,6 +4,10 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.AttributeSet;
@@ -14,14 +18,22 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
import org.joinmastodon.android.ui.text.CodeBlockSpan;
import org.joinmastodon.android.ui.text.DeleteWhenCopiedSpan;
import org.joinmastodon.android.ui.text.MonospaceSpan;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class LinkedTextView extends TextView{
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
private boolean needInvalidate;
private ActionMode currentActionMode;
private Paint bgPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Path tmpPath=new Path();
public LinkedTextView(Context context){
this(context, null);
@@ -56,6 +68,8 @@ public class LinkedTextView extends TextView{
currentActionMode=null;
}
});
bgPaint.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextContainer));
bgPaint.setPathEffect(new CornerPathEffect(V.dp(2)));
}
public boolean onTouchEvent(MotionEvent ev){
@@ -64,6 +78,22 @@ public class LinkedTextView extends TextView{
}
public void onDraw(Canvas c){
if(getText() instanceof Spanned spanned){
c.save();
c.translate(getTotalPaddingLeft(), getTotalPaddingTop());
Layout layout=getLayout();
MonospaceSpan[] monospaceSpans=spanned.getSpans(0, spanned.length(), MonospaceSpan.class);
for(MonospaceSpan span:monospaceSpans){
layout.getSelectionPath(spanned.getSpanStart(span), spanned.getSpanEnd(span), tmpPath);
c.drawPath(tmpPath, bgPaint);
}
CodeBlockSpan[] blockSpans=spanned.getSpans(0, spanned.length(), CodeBlockSpan.class);
for(CodeBlockSpan span:blockSpans){
c.drawRoundRect(V.dp(-4), layout.getLineTop(layout.getLineForOffset(spanned.getSpanStart(span)))-V.dp(8), layout.getWidth()+V.dp(4),
layout.getLineBottom(layout.getLineForOffset(spanned.getSpanEnd(span)))+V.dp(4), V.dp(2), V.dp(2), bgPaint);
}
c.restore();
}
super.onDraw(c);
delegate.onDraw(c);
if(needInvalidate)

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="20">
<path
android:fillColor="#000"
android:pathData="M23.933,2.824C22.324,4.079 21.073,5.357 20.179,6.657C19.33,7.912 18.905,9.1 18.905,10.221C19.084,10.131 19.307,10.086 19.575,10.086C19.888,10.041 20.156,10.019 20.38,10.019C21.408,10.019 22.257,10.445 22.927,11.297C23.642,12.103 24,13.112 24,14.322C24,15.802 23.508,17.035 22.525,18.021C21.542,19.007 20.313,19.5 18.838,19.5C17.274,19.5 16.045,18.94 15.151,17.819C14.257,16.653 13.81,15.107 13.81,13.179C13.81,10.893 14.503,8.629 15.888,6.388C17.274,4.147 19.307,2.017 21.989,0L23.933,2.824ZM10.123,2.824C8.514,4.079 7.263,5.357 6.369,6.657C5.52,7.912 5.095,9.1 5.095,10.221C5.274,10.131 5.497,10.086 5.765,10.086C6.078,10.041 6.346,10.019 6.57,10.019C7.598,10.019 8.447,10.445 9.117,11.297C9.832,12.103 10.19,13.112 10.19,14.322C10.19,15.802 9.698,17.035 8.715,18.021C7.732,19.007 6.503,19.5 5.028,19.5C3.464,19.5 2.235,18.94 1.341,17.819C0.447,16.653 0,15.107 0,13.179C0,10.893 0.693,8.629 2.078,6.388C3.464,4.147 5.497,2.017 8.179,0L10.123,2.824Z"/>
</vector>

View File

@@ -774,8 +774,14 @@ Zenbat eta jende gehiago jarraitu, orduan eta aktiboagoa eta interesgarriagoa iz
<item quantity="one">Erantsitako fitxategi %d</item>
<item quantity="other">%d erantsitako fitxategi</item>
</plurals>
<plurals name="poll_ended_x_voters">
<item quantity="one">%1$s erabiltzaileak inkesta bat egin zuen eta zuk eta beste erabiltzaile %2$,d bozkatu duzue</item>
<item quantity="other">%1$s erabiltzaileak inkesta bat egin zuen eta zuk eta beste %2$,d erabiltzailek bozkatu duzue</item>
</plurals>
<string name="own_poll_ended">Zure inkesta amaitu da</string>
<string name="user_just_posted">%s erabiltzaileak bidalketa egin berri du</string>
<string name="user_edited_post">%s(e)k interaktuatu zenuen argitalpen bat editatu du</string>
<string name="relationship_severance_account_suspension">%1$s zerbitzariko administratzaile batek %2$s bertan behera utzi du, hau da, ezin izango dituzu jaso hango eguneratzerik edo hangoekin elkarreragin.</string>
<!-- %1$s is your server domain, %2$s is the domain that was blocked, %3$,d is the follower count, %4$s is the `x_accounts` plural string -->
<plurals name="x_accounts">
<item quantity="one">kontu %,d</item>
@@ -789,5 +795,6 @@ Zenbat eta jende gehiago jarraitu, orduan eta aktiboagoa eta interesgarriagoa iz
<string name="moderation_warning_action_delete_statuses">Zure argitalpen batzuk kendu dira.</string>
<string name="moderation_warning_action_sensitive">Zure bidalketak hunkigarri gisa markatuko dira aurrerantzean.</string>
<string name="moderation_warning_action_silence">Zure kontuari mugak jarri zaizkio.</string>
<string name="moderation_warning_action_suspend">Zure kontua behin-behinean egotzi da.</string>
<string name="moderation_warning_learn_more">Informazio gehiago</string>
</resources>

View File

@@ -13,6 +13,8 @@
<string name="notifications">Notifikasi</string>
<string name="user_followed_you">%s mengikuti Anda</string>
<string name="user_sent_follow_request">%s mengirim Anda permintaan pengikut</string>
<string name="user_favorited">%s memfavorit:</string>
<string name="notification_boosted">%s membagikan ulang:</string>
<string name="share_toot_title">Bagikan</string>
<string name="settings">Pengaturan</string>
<string name="publish">Terbitkan</string>
@@ -730,6 +732,39 @@
<string name="donation_server_error">Maafkan kami, kesalahan telah terjadi dan kami belum bisa memproses donasi anda.\n\nMohon ulang kembali beberapa menit lagi.</string>
<string name="settings_donate">Donasi ke Mastodon</string>
<string name="settings_manage_donations">Kelola donasi</string>
<string name="cant_load_image">Tidak dapat memuat gambar</string>
<string name="poll_see_results">Lihat hasil</string>
<string name="poll_hide_results">Sembunyikan hasil</string>
<plurals name="x_attachments">
<item quantity="other">%d lampiran</item>
</plurals>
<plurals name="user_and_x_more_favorited">
<item quantity="other">%1$s dan %2$,d lainnya memfavorit:</item>
</plurals>
<plurals name="user_and_x_more_boosted">
<item quantity="other">%1$s dan %2$,d lainnya membagikan ulang:</item>
</plurals>
<plurals name="poll_ended_x_voters">
<item quantity="other">%1$s memulai japat yang Anda dan %2$,d lainnya beri suara</item>
</plurals>
<string name="own_poll_ended">Japat Anda telah berakhir</string>
<string name="user_just_posted">%s baru saja mengirim</string>
<string name="user_edited_post">%s menyunting kiriman yang Anda interaksi</string>
<string name="relationship_severance_account_suspension">Seorang admin dari %1$s telah menangguhkan %2$s, yang berarti Anda tidak dapat lagi menerima pembaruan atau berinteraksi.</string>
<!-- %1$s is your server domain, %2$s is the domain that was blocked, %3$,d is the follower count, %4$s is the `x_accounts` plural string -->
<string name="relationship_severance_domain_block">Seorang admin dari %1$s telah memblokir %2$s, termasuk %3$,d dari pengikut Anda dan %4$s lain yang Anda ikuti.</string>
<plurals name="x_accounts">
<item quantity="other">%,d akun</item>
</plurals>
<!-- %1$s is the domain that was blocked, %2$,d is the follower count, %3$s is the `x_accounts` plural string -->
<string name="relationship_severance_user_domain_block">Anda telah memblokir %1$s, menghapus %2$,d dari pengikut Anda dan %3$s lain yang Anda ikuti.</string>
<string name="relationship_severance_learn_more">Pelajari lebih lanjut</string>
<string name="moderation_warning_action_none">Akun Anda telah menerima peringatan moderasi.</string>
<string name="moderation_warning_action_disable">Akun Anda telah dinonaktifkan.</string>
<string name="moderation_warning_action_mark_statuses_as_sensitive">Beberapa kiriman Anda telah ditandai sebagai sensitif.</string>
<string name="moderation_warning_action_delete_statuses">Beberapa kiriman Anda telah dihapus.</string>
<string name="moderation_warning_action_sensitive">Kiriman Anda akan ditandai sebagai sensitif mulai dari sekarang.</string>
<string name="moderation_warning_action_silence">Akun Anda telah dibatasi.</string>
<string name="moderation_warning_action_suspend">Akun Anda telah ditangguhkan.</string>
<string name="moderation_warning_learn_more">Pelajari lebih lanjut</string>
</resources>

View File

@@ -464,7 +464,7 @@
<string name="notifications_policy_no_one">Никого</string>
<string name="settings_notifications_policy">Получать уведомления от</string>
<string name="notification_type_mentions_and_replies">Упоминания и ответы</string>
<string name="pause_all_notifications_title">Просмотреть все уведомления</string>
<string name="pause_all_notifications_title">Приостановить все уведомления</string>
<plurals name="x_weeks">
<item quantity="one">%d неделя</item>
<item quantity="few">%d недели</item>

View File

@@ -45,4 +45,11 @@
<color name="m3_sys_dark_on_surface_variant">@android:color/system_neutral2_200</color>
<color name="m3_sys_dark_outline">@android:color/system_neutral2_400</color>
<color name="m3_sys_dark_outline_variant">@android:color/system_neutral2_700</color>
<color name="ext_rich_text_text_light">@android:color/system_accent3_700</color>
<color name="ext_rich_text_container_light">@android:color/system_accent3_100</color>
<color name="ext_rich_text_decoration_light">@android:color/system_accent3_200</color>
<color name="ext_rich_text_text_dark">@android:color/system_accent3_200</color>
<color name="ext_rich_text_container_dark">@android:color/system_accent3_800</color>
<color name="ext_rich_text_decoration_dark">#0f0</color> <!-- it's "tertiary 35" but oh well -->
</resources>

View File

@@ -276,7 +276,7 @@
<string name="file_size_gb">%.2f GB</string>
<string name="upload_processing">正在处理…</string>
<!-- %s is file size -->
<string name="download_update">下载(%s)</string>
<string name="download_update">下载(%s)</string>
<string name="install_update">安装</string>
<string name="privacy_policy_title">你的隐私</string>
<string name="privacy_policy_subtitle">尽管 Mastodon 客户端本身不采集任何数据,你注册的实例可能会有不同的隐私政策。\n\n如果你不同意 %s 的这些政策,请返回并选择其他实例。</string>

View File

@@ -33,6 +33,9 @@
<attr name="colorWhite" format="color"/>
<attr name="colorFavorite" format="color" />
<attr name="colorBoost" format="color" />
<attr name="colorRichTextText" format="color"/>
<attr name="colorRichTextContainer" format="color"/>
<attr name="colorRichTextDecorations" format="color"/>
<declare-styleable name="MaxWidthFrameLayout">
<attr name="android:maxWidth" format="dimension"/>

View File

@@ -104,5 +104,12 @@
<color name="ext_on_bookmark_container_light_medium_contrast">#FFFFFF</color>
<color name="ext_on_bookmark_container_dark">#FFFFFF</color>
<color name="ext_rich_text_text_light">#6C3646</color>
<color name="ext_rich_text_container_light">#FFD9E1</color>
<color name="ext_rich_text_decoration_light">#FDB2C5</color>
<color name="ext_rich_text_text_dark">#FDB2C5</color>
<color name="ext_rich_text_container_dark">#512030</color>
<color name="ext_rich_text_decoration_dark">#7A4152</color>
<item name="overlay_ripple_alpha" format="float" type="dimen">0.12</item>
</resources>

View File

@@ -58,6 +58,9 @@
<item name="colorWhite">#FFF</item>
<item name="colorFavorite">@color/ext_favorite_light</item>
<item name="colorBoost">@color/ext_boost_light</item>
<item name="colorRichTextText">@color/ext_rich_text_text_light</item>
<item name="colorRichTextContainer">@color/ext_rich_text_container_light</item>
<item name="colorRichTextDecorations">@color/ext_rich_text_decoration_light</item>
<item name="android:statusBarColor">?colorM3Background</item>
<item name="android:navigationBarColor">@color/navigation_bar_bg_light</item>
@@ -126,6 +129,9 @@
<item name="colorWhite">#000</item>
<item name="colorFavorite">@color/ext_favorite_dark</item>
<item name="colorBoost">@color/ext_boost_dark</item>
<item name="colorRichTextText">@color/ext_rich_text_text_dark</item>
<item name="colorRichTextContainer">@color/ext_rich_text_container_dark</item>
<item name="colorRichTextDecorations">@color/ext_rich_text_decoration_dark</item>
<item name="android:statusBarColor">?colorM3Background</item>
<item name="android:navigationBarColor">?colorM3Background</item>