Compare commits

...

25 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
Grishka
71f92cb66c Bump version 2024-10-06 01:25:21 +03:00
Grishka
77b2abd0cb Merge branch 'l10n_master' 2024-10-06 01:24:29 +03:00
Grishka
15385dd924 Make fastlane create draft releases 2024-10-06 01:22:25 +03:00
Grishka
08847ec641 Always reset notifications marker on "mark as read"
fixes #897
2024-10-06 01:11:21 +03:00
Grishka
805fc5d8c7 Crash fix 2024-10-06 01:06:45 +03:00
Eugen Rochko
3d7a95d336 New translations strings.xml (German) 2024-10-05 09:01:18 +02:00
Eugen Rochko
c1869386ff New translations strings.xml (German) 2024-10-05 07:34:09 +02:00
Eugen Rochko
7a728c52cf New translations strings.xml (German) 2024-10-04 21:23:57 +02:00
Eugen Rochko
22f3aad538 New translations strings.xml (Frisian) 2024-10-04 18:40:52 +02:00
Eugen Rochko
42da6dcf48 New translations strings.xml (Dutch) 2024-10-04 18:40:50 +02:00
Eugen Rochko
c0f18b1f61 New translations strings.xml (Frisian) 2024-10-04 17:34:12 +02:00
47 changed files with 570 additions and 53 deletions

View File

@@ -28,7 +28,7 @@ platform :android do
build_type: "release",
)
upload_to_play_store(
changes_not_sent_for_review: true,
release_status: "draft",
skip_upload_images: true,
skip_upload_screenshots: true
)

View File

@@ -13,8 +13,8 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 34
versionCode 120
versionName "2.7.1"
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.1'
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

@@ -241,7 +241,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
markAsRead(true);
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
@@ -257,11 +257,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
return mergeAdapter;
}
private void markAsRead(){
private void markAsRead(boolean force){
if(data.isEmpty())
return;
String id=data.get(0).notification.pageMaxId;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
if(force || ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
@@ -290,7 +290,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
return;
for(NotificationViewModel n:items){
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
markAsRead();
markAsRead(false);
break;
}
}

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

@@ -236,6 +236,8 @@ public class ThreadFragment extends StatusListFragment{
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
if(holder instanceof StatusDisplayItem.Holder<?> statusHolder && siblingHolder instanceof StatusDisplayItem.Holder<?> siblingStatusHolder){
Status siblingStatus=getStatusByID(siblingStatusHolder.getItemID());
if(siblingStatus==null)
return;
if(statusHolder.getItemID().equals(siblingStatus.inReplyToId) && siblingStatus!=mainStatus && !statusHolder.getItemID().equals(mainStatus.id))
return;
}
@@ -291,6 +293,7 @@ public class ThreadFragment extends StatusListFragment{
continue;
float lineX=V.dp(36);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.save();
c.clipRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
if(holder instanceof HeaderStatusDisplayItem.Holder){

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){
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

@@ -13,8 +13,8 @@
<string name="notifications">Benachrichtigungen</string>
<string name="user_followed_you">%s folgt dir jetzt</string>
<string name="user_sent_follow_request">%s hat dir eine Folgeanfrage gesendet</string>
<string name="user_favorited">%s als Favorit markiert:</string>
<string name="notification_boosted">%s verstärkt:</string>
<string name="user_favorited">%s favorisierte:</string>
<string name="notification_boosted">%s teilte deinen Beitrag:</string>
<string name="share_toot_title">Teilen</string>
<string name="settings">Einstellungen</string>
<string name="publish">Veröffentlichen</string>
@@ -216,8 +216,8 @@
<string name="followed_user">Du folgst nun %s</string>
<string name="following_user_requested">Deine Follower-Anfrage an %s wurde gesendet</string>
<string name="open_in_browser">Im Browser öffnen</string>
<string name="hide_boosts_from_user">Boosts ausblenden</string>
<string name="show_boosts_from_user">Boosts anzeigen</string>
<string name="hide_boosts_from_user">Geteilte Beiträge ausblenden</string>
<string name="show_boosts_from_user">Geteilte Beiträge anzeigen</string>
<string name="signup_reason">Warum möchtest du beitreten?</string>
<string name="signup_reason_note">Das erleichtert uns die Prüfung deiner Anmeldung.</string>
<string name="clear">Leeren</string>
@@ -769,7 +769,41 @@
<string name="cant_load_image">Bild konnte nicht geladen werden</string>
<string name="poll_see_results">Ergebnisse anzeigen</string>
<string name="poll_hide_results">Ergebnisse verstecken</string>
<plurals name="x_attachments">
<item quantity="one">%d Anhang</item>
<item quantity="other">%d Anhänge</item>
</plurals>
<plurals name="user_and_x_more_favorited">
<item quantity="one">%1$s und %2$,d weitere Person favorisierten:</item>
<item quantity="other">%1$s und %2$,d weitere Personen favorisierten:</item>
</plurals>
<plurals name="user_and_x_more_boosted">
<item quantity="one">%1$s und %2$,d weitere Person haben geteilt:</item>
<item quantity="other">%1$s und %2$,d weitere Personen haben geteilt:</item>
</plurals>
<plurals name="poll_ended_x_voters">
<item quantity="one">%1$s hat eine Umfrage durchgeführt, an der du und %2$,d weitere Person teilgenommen haben</item>
<item quantity="other">%1$s hat eine Umfrage durchgeführt, an der du und %2$,d weitere Personen teilgenommen haben</item>
</plurals>
<string name="own_poll_ended">Ihre Umfrage ist beendet</string>
<string name="user_just_posted">%s hat gerade gepostet</string>
<string name="user_edited_post">%s hat einen Beitrag bearbeitet, mit dem du interagiert hast</string>
<string name="relationship_severance_account_suspension">Ein Admin von %1$s hat %2$s gesperrt. Du wirst von diesem Profil keine Updates mehr erhalten und auch nicht mit ihm interagieren können.</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">Ein Admin von %1$s hat %2$s blockiert darunter %3$,d deiner Follower und %4$s Konten, denen du folgst.</string>
<plurals name="x_accounts">
<item quantity="one">%,d Konto</item>
<item quantity="other">%,d Konten</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">Du hast %1$s blockiert. %2$,d deiner Follower und %3$s Konten, denen du folgst, wurden entfernt.</string>
<string name="relationship_severance_learn_more">Mehr erfahren</string>
<string name="moderation_warning_action_none">Dein Konto ist von den Moderator*innen verwarnt worden.</string>
<string name="moderation_warning_action_disable">Dein Konto wurde eingeschränkt.</string>
<string name="moderation_warning_action_mark_statuses_as_sensitive">Einige deiner Beiträge wurden mit einer Inhaltswarnung versehen.</string>
<string name="moderation_warning_action_delete_statuses">Einige deiner Beiträge sind entfernt worden.</string>
<string name="moderation_warning_action_sensitive">Deine zukünftigen Beiträge werden mit einer Inhaltswarnung versehen.</string>
<string name="moderation_warning_action_silence">Dein Konto wurde eingeschränkt.</string>
<string name="moderation_warning_action_suspend">Dein Konto wurde gesperrt.</string>
<string name="moderation_warning_learn_more">Mehr erfahren</string>
</resources>

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">Meldingen</string>
<string name="user_followed_you">%s folget jo no</string>
<string name="user_sent_follow_request">%s wol jo graach folgje</string>
<string name="user_favorited">%s markearre as favoryt:</string>
<string name="notification_boosted">%s boostte:</string>
<string name="share_toot_title">Diele</string>
<string name="settings">Ynstellingen</string>
<string name="publish">Publisearje</string>
@@ -382,6 +384,12 @@
<string name="alt_text">Alternative tekst</string>
<string name="help">Help</string>
<string name="what_is_alt_text">Wat is alternative tekst?</string>
<string name="alt_text_help">Alt-tekst biedt ôfbyldingsbeskriuwingen foar minsken mei in fisuele beheining en ferbiningen mei in lege bânbreedte of foar minsken dyt nei ekstra kontekst sykje.\n\nJo kinne de tagonklikheid en de begryplikheid foar elkenien ferbetterje troch dúdlik, koart en objektyf te skriuwen.\n\n<ul><li>Beskriuw wichtige eleminten</li>\n<li>Fetsje tekst yn ôfbyldingen gear</li>\n<li>Brûk in normale sinsbou</li>\n<li>Mij oertallige ynformaasje</li>\n<li>Fokusje op trends en wichtige befiningen yn komplekse bylden (lykas diagrammen of kaarten)</li></ul> \n</string>
<string name="edit_post">Berjocht bewurkje</string>
<string name="no_verified_link">Gjin ferifiearre keppeling</string>
<string name="compose_autocomplete_emoji_empty">Emoji trochsykje</string>
<string name="compose_autocomplete_users_empty">Fyn wat jo sykje</string>
<string name="no_search_results">Dizze syktermen leverje gjin resultaat op</string>
<string name="language">Taal</string>
<string name="language_default">Standert</string>
<string name="language_system">Systeem</string>
@@ -391,21 +399,78 @@
<string name="media_hidden">Media ferstoppe</string>
<string name="post_hidden">Berjocht ferstoppe</string>
<string name="report_title_post">Berjocht rapportearje</string>
<string name="forward_report_explanation">De account stiet op in oare server. Wolle jo dêr ek in anonimisearre kopy fan dit rapport nei ta stjoere?</string>
<!-- %s is the server domain -->
<string name="forward_report_to_server">Nei %s trochstjoere</string>
<!-- Shown on the "stamp" on the screen that appears after you report a post/user. Please keep the translation short, preferably a single word -->
<string name="reported">Rapportearre</string>
<string name="report_unfollow_explanation">As jo harren berjochten net mear op jo starttiidline sjen wolle, moatte jo dizze persoan net mear folgje.</string>
<string name="muted_user">%s negearje</string>
<string name="report_sent_already_blocked">Jo hawwe dizze brûker al blokkearre, dus jo hoege neat mear te dwaan wylst wy jo rapport beoardiele.</string>
<string name="report_personal_already_blocked">Jo hawwe dizze brûker al blokkearre, dus der is neat oars dat jo hoege te dwaan.\n\nTank foar jo help om Mastodon foar elkenien feilich te hâlden!</string>
<string name="blocked_user">%s blokkearre</string>
<string name="mark_all_notifications_read">Alles as lêzen markearje</string>
<string name="settings_display">Werjefte</string>
<string name="settings_filters">Filters</string>
<string name="settings_server_explanation">Oersjoch, rigels, moderators</string>
<!-- %s is the app name (Mastodon, key app_name). I made it a placeholder so everything Just Works™ with forks -->
<string name="about_app">Oer %s</string>
<string name="default_post_language">Standert taal foar berjochten</string>
<string name="settings_alt_text_reminders">Alt-tekst-omtinken tafoegje</string>
<string name="settings_confirm_unfollow">Eardat jo ien ûntfolgje om befêstiging freegje</string>
<string name="settings_confirm_boost">Eardat jo in berjocht booste om befêstiging freegje</string>
<string name="settings_confirm_delete_post">Eardat jo in berjocht fuortsmite om befêstiging freegje</string>
<string name="pause_all_notifications">Alles pauzearje</string>
<string name="pause_notifications_off">Ut</string>
<string name="notifications_policy_anyone">Elkenien</string>
<string name="notifications_policy_followed">Minsken dyt jo folgje</string>
<string name="notifications_policy_follower">Minsken dyt josels folgje</string>
<string name="notifications_policy_no_one">Net ien</string>
<string name="settings_notifications_policy">Meldingen ûntfange fan</string>
<string name="notification_type_mentions_and_replies">Fermeldingen en reaksjes</string>
<string name="pause_all_notifications_title">Alle meldingen pauzearje</string>
<plurals name="x_weeks">
<item quantity="one">%d wike</item>
<item quantity="other">%d wiken</item>
</plurals>
<!-- %1$s is the date (may be relative, e.g. "today" or "yesterday"), %2$s is the time. You can reorder these placeholders if that works better for your language -->
<string name="date_at_time">%1$s om %2$s</string>
<string name="today">hjoed</string>
<string name="yesterday">juster</string>
<string name="tomorrow">moarn</string>
<!-- %s is the timestamp ("tomorrow at 12:34") -->
<string name="pause_notifications_ends">Einiget om %s</string>
<!-- %s is the timestamp ("tomorrow at 12:34") -->
<string name="pause_notifications_banner">Meldingen wurde om %s ferfette.</string>
<string name="resume_notifications_now">No ferfetsje</string>
<string name="open_system_notification_settings">Gean nei meldingsynstellingen</string>
<string name="about_server">Oer</string>
<string name="server_rules">Regels</string>
<string name="server_administrator">Behearder</string>
<string name="send_email_to_server_admin">Berjocht oan behearder</string>
<string name="notifications_disabled_in_system">Meldingen yn jo apparaatynstellingen ynskeakelje om oeral fernijingen sjen te kinnen.</string>
<string name="settings_even_more">Noch mear ynstellingen</string>
<string name="settings_show_cws">Ynhâldswarskôgingen toane</string>
<string name="settings_hide_sensitive_media">As gefoelich markearre media ferstopje</string>
<string name="settings_show_interaction_counts">Oantal berjochtynteraksjes</string>
<string name="settings_show_emoji_in_names">Oanpaste emoji yn werjeftenammen</string>
<plurals name="in_x_seconds">
<item quantity="one">yn %d sekonde</item>
<item quantity="other">yn %d sekonden</item>
</plurals>
<plurals name="in_x_minutes">
<item quantity="one">yn %d minút</item>
<item quantity="other">yn %d minuten</item>
</plurals>
<plurals name="in_x_hours">
<item quantity="one">yn %d oere</item>
<item quantity="other">yn %d oeren</item>
</plurals>
<plurals name="x_hours_ago">
<item quantity="one">%d oere lyn</item>
<item quantity="other">%d oeren lyn</item>
</plurals>
<string name="alt_text_reminder_title">Media sûnder alt-tekst</string>
<string name="count_one">Ien</string>
<string name="count_two">Twa</string>
<string name="count_three">Trije</string>

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

@@ -725,7 +725,7 @@
<string name="notification_filter_new_accounts">Nieuwe accounts</string>
<string name="notification_filter_new_accounts_explanation">In de afgelopen 30 dagen geregistreerd</string>
<string name="notification_filter_mentions">Ongevraagde privéberichten</string>
<string name="notification_filter_mentions_explanation">Onzichtbaar tenzij het een antwoord is op een privébericht van jou of wanneer je de afzender volgt</string>
<string name="notification_filter_mentions_explanation">Onzichtbaar tenzij het een reactie is op een privébericht van jou of wanneer je de afzender volgt</string>
<string name="allow_notifications">Meldingen toestaan</string>
<string name="mute_notifications">Meldingen afwijzen</string>
<plurals name="x_people_you_may_know">
@@ -786,8 +786,24 @@
<item quantity="other">%1$s hield een peiling, waaraan jij en %2$,d andere personen hebben meegedaan</item>
</plurals>
<string name="own_poll_ended">Jouw peiling is beëindigd</string>
<string name="user_just_posted">%s heeft zojuist een bericht geplaatst</string>
<string name="user_edited_post">%s bewerkte een bericht waarmee je interactie had</string>
<string name="relationship_severance_account_suspension">Een beheerder van %1$s heeft %2$s geschorst, wat betekent dat je geen updates meer van hen kunt ontvangen of met hen kunt communiceren.</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">Een beheerder van %1$s heeft %2$s geblokkeerd, inclusief %3$,d van jouw volgers en %4$s account(s) die je volgt.</string>
<plurals name="x_accounts">
<item quantity="one">%,d account</item>
<item quantity="other">%,d accounts</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">Je hebt %1$s geblokkeerd, waarmee je %2$,d van je volgers en %3$s account(s) die je volgt, bent verloren.</string>
<string name="relationship_severance_learn_more">Meer informatie</string>
<string name="moderation_warning_action_none">Jouw account heeft een moderatie-waarschuwing ontvangen.</string>
<string name="moderation_warning_action_disable">Jouw account is uitgeschakeld.</string>
<string name="moderation_warning_action_mark_statuses_as_sensitive">Sommige van je berichten zijn gemarkeerd als gevoelig.</string>
<string name="moderation_warning_action_delete_statuses">Sommige van je berichten zijn verwijderd.</string>
<string name="moderation_warning_action_sensitive">Je berichten worden vanaf nu als gevoelig gemarkeerd.</string>
<string name="moderation_warning_action_silence">Jouw account is beperkt.</string>
<string name="moderation_warning_action_suspend">Jouw account is opgeschort.</string>
<string name="moderation_warning_learn_more">Meer informatie</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>