Compare commits

...

47 Commits

Author SHA1 Message Date
sk
b94741feae boop version 2023-06-10 22:10:24 +02:00
sk
e43d6c35d8 update languages 2023-06-10 22:10:13 +02:00
sk
4a6f9e80b1 Merge remote-tracking branch 'weblate/main' 2023-06-10 22:03:41 +02:00
gallegonovato
ec02680507 Translated using Weblate (Spanish)
Currently translated at 99.6% (300 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-10 20:03:34 +00:00
sk22
5fc569a45a Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 20:03:34 +00:00
sk
4bc9c5691d Merge remote-tracking branch 'upstream/l10n_master' 2023-06-10 22:03:19 +02:00
sk
19b68855ac move permission definition to status privacy 2023-06-10 21:54:47 +02:00
sk
70fdfb612e fix issue on re-bind 2023-06-10 21:54:18 +02:00
sk
0a32c217d8 Merge branch 'main' into pr/FineFindus/557 2023-06-10 21:47:26 +02:00
Eugen Rochko
5dfa9237ad New translations short_description.txt (Persian) 2023-06-10 21:44:47 +02:00
sk
573ff75498 update ancestor when deleting post 2023-06-10 21:32:41 +02:00
sk
87c37df370 insert replied directly below status
closes sk22#558
2023-06-10 20:57:01 +02:00
sk22
7fb0944e66 Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 16:59:16 +00:00
sk
35c8a3d121 change strings 2023-06-10 18:59:05 +02:00
sk
9e58413d1a Merge remote-tracking branch 'weblate/main' 2023-06-10 18:54:32 +02:00
sk
90e60aef84 change auto-reveal cw wording
closes sk22#562
2023-06-10 18:53:38 +02:00
sk
8547ce05ed change auto-reveal cw wording
closes sk22#562
2023-06-10 18:52:01 +02:00
gicorada
0825faee5c Translated using Weblate (Italian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-06-10 16:42:23 +00:00
Linerly
d43a697df7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-10 16:42:23 +00:00
Choukajohn
3b742c4391 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
ihor_ck
a43a396043 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-10 16:42:23 +00:00
Choukajohn
bcb4fac553 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
sk
35bf858a83 auto-reveal equal spoilers in threads 2023-06-09 14:54:03 +02:00
sk
870bfaf08c don't use switch for android ids 2023-06-09 14:50:21 +02:00
sk
c4238fb19b keep revealed states when reloading
closes sk22#561
2023-06-09 13:27:25 +02:00
sk
ba7aeb358b fix thread status not clickable if filter revealed
closes sk22#554
2023-06-09 12:50:10 +02:00
sk
6f3fd4d454 fix opening browser twice
closes sk22#559
2023-06-09 12:47:32 +02:00
Eugen Rochko
c890195567 New translations strings.xml (Swedish) 2023-06-08 22:57:52 +02:00
Eugen Rochko
b50a327b17 New translations strings.xml (Swedish) 2023-06-08 17:47:06 +02:00
sk
97547f334f returning optionals for the optional god
closes sk22#555
2023-06-08 15:31:58 +02:00
sk
1ab953d819 fix spoiler button being hidden while editing
closes sk22#553
2023-06-08 15:03:04 +02:00
FineFindus
dbe7eb25ff Merge branch 'main' into feat/hide-non-boostable-boosts 2023-06-08 10:35:12 +02:00
FineFindus
45ecec09f5 feat: hide boost count on non-boostable statuses 2023-06-08 10:33:27 +02:00
FineFindus
9b4556d293 refactor: move boostable check to status 2023-06-08 10:33:27 +02:00
sk
307d483a56 add comment 2023-06-07 21:59:56 +02:00
sk
9612248695 remove unused imports 2023-06-07 21:51:17 +02:00
sk
1f63401e5b fix pixelfed post editing 2023-06-07 21:50:50 +02:00
sk
d35ec18a88 increase akkoma scheduled posts compatibility 2023-06-07 21:12:38 +02:00
sk
b93b1847c3 increase pixelfed compatibility 2023-06-07 21:12:30 +02:00
sk
cd46ed565f open browser if login redirects to website 2023-06-07 21:11:20 +02:00
sk
4a0e4edef8 load more if screen isn't full yet 2023-06-07 20:28:56 +02:00
sk
2ea7333daa only reload main status when refreshing
closes sk22#552
2023-06-07 18:45:50 +02:00
sk
fa7a66809d change profile loading error behavior 2023-06-07 16:09:41 +02:00
sk
71884ab760 don't show wrong relationship with remote accounts 2023-06-07 15:50:30 +02:00
sk
f31205c670 generalize lookup error handling 2023-06-07 15:45:22 +02:00
sk
0091ae87ce fix issues with external share
closes sk22#550
2023-06-07 15:17:44 +02:00
sk
ad13b1e927 bump version 2023-06-06 17:13:30 +02:00
38 changed files with 466 additions and 144 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 90
versionName "1.2.3+fork.90"
versionCode 92
versionName "1.2.3+fork.92"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}

View File

@@ -9,6 +9,7 @@ import android.text.TextUtils;
import android.util.Pair;
import android.widget.Toast;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
@@ -43,7 +44,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish();
} else if (isOpenable || sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
if (isOpenable) sheet.setOnClick((accountId, open) -> {
sheet.setOnClick((accountId, open) -> {
if (open && text.isPresent()) {
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
if (clazz == null) {
@@ -59,8 +60,17 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish();
startActivity(intent);
};
if (isFediUrl) UiUtils.lookupURL(this, accountId, text.get(), false, callback);
else UiUtils.lookupAccountHandle(this, accountId, fediHandle.get(), callback);
fediHandle
.<MastodonAPIRequest<?>>map(handle ->
UiUtils.lookupAccountHandle(this, accountId, handle, callback))
.or(() ->
UiUtils.lookupURL(this, accountId, text.get(), callback))
.ifPresent(req ->
req.wrapProgress(this, R.string.loading, true, d -> {
UiUtils.transformDialogForLookup(this, accountId, isFediUrl ? text.get() : null, d);
d.setOnDismissListener((ev) -> finish());
}));
} else {
openComposeFragment(accountId);
}

View File

@@ -49,6 +49,7 @@ public class GlobalUserPreferences{
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static AutoRevealMode autoRevealEqualSpoilers;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
@@ -129,6 +130,7 @@ public class GlobalUserPreferences{
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -179,6 +181,7 @@ public class GlobalUserPreferences{
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.apply();
}
@@ -198,4 +201,10 @@ public class GlobalUserPreferences{
LIGHT,
DARK
}
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
}

View File

@@ -127,8 +127,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}
private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) {
String className = args.getString("fromExternalShare");
Fragment fragment = switch (className) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;

View File

@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
@Override
public void onSuccess(Token token){
new GetOwnAccount()
// in case the instance (looking at pixelfed) wants to redirect to a
// website, we need to pass a context so we can launch a browser
.setContext(OAuthActivity.this)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -161,6 +162,11 @@ public class MastodonAPIController{
respObj=gson.fromJson(reader, req.respClass);
}
}catch(JsonIOException|JsonSyntaxException x){
if (req.context != null && response.body().contentType().subtype().equals("html")) {
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
req.cancel();
return;
}
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
@@ -50,6 +51,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Map<String, String> headers;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@Nullable Context context;
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
this.path=path;
@@ -181,6 +183,16 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> setContext(Context context) {
this.context = context;
return this;
}
@Nullable
public Context getContext() {
return context;
}
@CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){

View File

@@ -754,6 +754,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
@Override
protected void onDataLoaded(List<T> d, boolean more) {
super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){

View File

@@ -149,7 +149,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -355,6 +355,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -847,12 +848,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
}
@Override
public String getAccountID() {
return accountID;
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -1009,7 +1016,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.state!=AttachmentUploadState.DONE)
nonDoneAttachmentCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE);
}
@@ -1151,7 +1158,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sendProgress.setVisibility(View.VISIBLE);
sendError.setVisibility(View.GONE);
Callback<Status> resCallback=new Callback<>(){
Callback<Status> resCallback = new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
@@ -1164,7 +1171,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}else{
E.post(new StatusUpdatedEvent(result));
// pixelfed doesn't return the edited status :/
Status editedStatus = result == null ? editingStatus : result;
if (result == null) {
editedStatus.text = req.status;
editedStatus.spoilerText = req.spoilerText;
editedStatus.sensitive = req.sensitive;
editedStatus.language = req.language;
// user will have to reload to see html
editedStatus.content = req.status;
}
E.post(new StatusUpdatedEvent(editedStatus));
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
Nav.finish(ComposeFragment.this);
@@ -1899,9 +1916,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
if (isInstancePixelfed()) {
m.findItem(R.id.vis_private).setVisible(false);
}
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.isAkkoma()) {
if (isInstanceAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@@ -17,6 +17,10 @@ public interface HasAccountID {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default boolean isInstancePixelfed() {
return getInstance().map(Instance::isPixelfed).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}

View File

@@ -384,10 +384,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
if (getContext() == null) return;
if (args == null || !args.containsKey("profileAccount")) {
onError(new MastodonErrorResponse(
getContext().getString(R.string.sk_error_loading_profile),
0, null
));
Toast.makeText(getContext(), getContext().getString(
R.string.sk_error_loading_profile, domain
), Toast.LENGTH_SHORT).show();
Nav.finish(this);
return;
}
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
@@ -654,8 +654,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
));
}
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
if(isOwnProfile)
if(isOwnProfile) {
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
return;
}
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));

View File

@@ -37,6 +37,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
@@ -85,8 +86,8 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
private ButtonItem defaultContentTypeButtonItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -189,9 +190,18 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.showInteractionCounts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
items.add(alwaysRevealSpoilersItem = new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save();
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
}));
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
onAutoRevealSpoilerChanged(b);
}));
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
GlobalUserPreferences.disableSwipe=i.checked;
@@ -276,7 +286,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.collapseLongPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
GlobalUserPreferences.spectatorMode=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
@@ -531,6 +541,36 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
return true;
}
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
int id = item.getItemId();
AutoRevealMode mode = AutoRevealMode.NEVER;
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
GlobalUserPreferences.alwaysExpandContentWarnings = false;
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
GlobalUserPreferences.save();
onAutoRevealSpoilerChanged(btn);
return true;
}
private void onAutoRevealSpoilerChanged(Button b) {
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
b.setText(R.string.sk_settings_auto_reveal_anyone);
} else {
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
case THREADS -> R.string.sk_settings_auto_reveal_author;
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
default -> R.string.sk_settings_auto_reveal_nobody;
});
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
}
}
}
private void onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
@@ -560,14 +600,14 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private boolean onContentTypeChanged(MenuItem item, Button btn){
int id = item.getItemId();
ContentType contentType = switch (id) {
case R.id.content_type_plain -> ContentType.PLAIN;
case R.id.content_type_html -> ContentType.HTML;
case R.id.content_type_markdown -> ContentType.MARKDOWN;
case R.id.content_type_bbcode -> ContentType.BBCODE;
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
default -> null;
};
ContentType contentType = null;
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
GlobalUserPreferences.save();
btn.setText(getContentTypeString(contentType));

View File

@@ -164,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected void removeStatus(Status status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
StatusDisplayItem item = displayItems.get(i);
if(status.id.equals(item.parentID)){
index=i;
break;
}
if (item.parentID.equals(status.inReplyToId)) {
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
ancestorLastIndex = i;
}
}
// did we find an ancestor that is also the status' neighbor?
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
StatusDisplayItem item = displayItems.get(i);
// update ancestor to have no descendant anymore
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
}
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
}
if(index==-1)
return;
int lastIndex;

View File

@@ -8,6 +8,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
@@ -37,6 +39,7 @@ import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -102,22 +105,26 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
footer.hideCounts=true;
}
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
return items;
}
@Override
protected void doLoadData(int offset, int count){
loadMainStatus();
if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if (getContext() == null) return;
Map<String, Status> oldData = null;
if(refreshing){
oldData = new HashMap<>(data.size());
for (Status s : data) oldData.put(s.id, s);
data.clear();
ancestryMap.clear();
displayItems.clear();
@@ -149,6 +156,23 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
adapter.notifyItemRemoved(prependedCount);
count--;
}
for (Status s : data) {
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.filterRevealed = oldStatus.filterRevealed;
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText) &&
mainStatus.spoilerRevealed) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = true;
}
}
}
dataLoaded();
if(refreshing){
refreshDone();
@@ -187,6 +211,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
protected Object maybeApplyMainStatus() {
if (updatedStatus == null || !contextInitiallyRendered) return null;
// restore revealed states for main status because it gets updated after doLoadData
updatedStatus.filterRevealed = mainStatus.filterRevealed;
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
// returning fired event object to facilitate testing
Object event;
if (updatedStatus.editedAt != null &&
@@ -310,15 +338,65 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
data.add(ev.status);
onAppendItems(Collections.singletonList(ev.status));
if (ev.status.inReplyToId == null) return;
Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
if (repliedToStatus == null) return;
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
int nextDisplayItemsIndex = -1, indexOfPreviousDisplayItem = -1;
for (int i = 0; i < displayItems.size(); i++) {
StatusDisplayItem item = displayItems.get(i);
if (repliedToStatus.id.equals(item.parentID)) {
// saving the replied-to status' display items index to eventually reach the last one
indexOfPreviousDisplayItem = i;
item.hasDescendantNeighbor = true;
} else if (indexOfPreviousDisplayItem >= 0 && nextDisplayItemsIndex == -1) {
// previous display item was the replied-to status' display items
nextDisplayItemsIndex = i;
// nothing left to do if there's no other reply to that status
if (ancestry.descendantNeighbor == null) break;
}
if (ancestry.descendantNeighbor != null && item.parentID.equals(ancestry.descendantNeighbor.id)) {
// existing reply shall no longer have the replied-to status as its neighbor
item.hasAncestoringNeighbor = false;
}
}
// fall back to inserting the item at the end
nextDisplayItemsIndex = nextDisplayItemsIndex >= 0 ? nextDisplayItemsIndex : displayItems.size();
int nextDataIndex = data.indexOf(repliedToStatus) + 1;
// if replied-to status already has another reply...
if (ancestry.descendantNeighbor != null) {
// update the reply's ancestry to remove its ancestoring neighbor (as we did above)
ancestryMap.get(ancestry.descendantNeighbor.id).ancestoringNeighbor = null;
// make sure the existing reply has a reply line
if (nextDataIndex < data.size() &&
!(displayItems.get(nextDisplayItemsIndex) instanceof ReblogOrReplyLineStatusDisplayItem)) {
Status nextStatus = data.get(nextDataIndex);
if (!nextStatus.account.id.equals(repliedToStatus.account.id)) {
// create reply line manually since we're not building that status' items
displayItems.add(nextDisplayItemsIndex, StatusDisplayItem.buildReplyLine(
this, nextStatus, accountID, nextStatus, repliedToStatus.account, false
));
}
}
}
// update replied-to status' ancestry
ancestry.descendantNeighbor = ev.status;
// add ancestry for newly created status before building its display items
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
data.add(nextDataIndex, ev.status);
adapter.notifyDataSetChanged();
}
@Override
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
}
@Override

View File

@@ -257,7 +257,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);

View File

@@ -65,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
/**
* An image banner that is shown above the profile and in profile cards.
*/
@RequiredField
public String header;
/**
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.

View File

@@ -148,6 +148,10 @@ public class Instance extends BaseModel{
return pleroma != null;
}
public boolean isPixelfed() {
return version.contains("compatible; Pixelfed");
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
@@ -16,7 +17,6 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
@@ -24,8 +24,17 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
return id;
}
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of();
for(Attachment a:mediaAttachments)
a.postprocess();
if (params != null) params.postprocess();
}
@Parcel
public static class Params {
public static class Params extends BaseModel {
@RequiredField
public String text;
public String spoilerText;
@@ -40,10 +49,16 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public String applicationId;
public List<String> mediaIds;
public ContentType contentType;
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (poll != null) poll.postprocess();
}
}
@Parcel
public static class ScheduledPoll {
public static class ScheduledPoll extends BaseModel {
@RequiredField
public String expiresIn;
@RequiredField

View File

@@ -12,6 +12,7 @@ import com.google.gson.JsonParseException;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
@@ -20,8 +21,6 @@ import java.lang.reflect.Type;
import java.time.Instant;
import java.util.List;
import androidx.annotation.NonNull;
@Parcel
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
@RequiredField
@@ -39,7 +38,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public boolean sensitive;
@RequiredField
public String spoilerText;
@RequiredField
public List<Attachment> mediaAttachments;
public Application application;
@RequiredField
@@ -96,6 +94,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
t.postprocess();
for(Emoji e:emojis)
e.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of();
for(Attachment a:mediaAttachments)
a.postprocess();
account.postprocess();
@@ -173,6 +172,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
return strippedText;
}
public boolean isReblogPermitted(String accountID){
return visibility.isReblogPermitted(account.id.equals(
AccountSessionManager.getInstance().getAccount(accountID).self.id
));
}
public static Status ofFake(String id, String text, Instant createdAt) {
Status s = new Status();
s.id = id;

View File

@@ -14,7 +14,7 @@ public enum StatusPrivacy{
@SerializedName("local")
LOCAL(4); // akkoma
private int privacy;
private final int privacy;
StatusPrivacy(int privacy) {
this.privacy = privacy;
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
return privacy > other.getPrivacy();
}
public boolean isReblogPermitted(boolean isOwnStatus){
return (this == StatusPrivacy.PUBLIC ||
this == StatusPrivacy.UNLISTED ||
this == StatusPrivacy.LOCAL ||
(this == StatusPrivacy.PRIVATE && isOwnStatus));
}
public int getPrivacy() {
return privacy;
}

View File

@@ -75,8 +75,7 @@ public class AccountSwitcherSheet extends BottomSheet{
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);

View File

@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final String accountID;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Status status){
super(parentID, parentFragment);
this.status=status;
this.accountID=accountID;
}
@Override
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int)(s.reblogsCount%1000), s.reblogsCount));
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
reblogs.setVisibility(s.isReblogPermitted(item.accountID) ? View.VISIBLE : View.GONE);
if(s.editedAt!=null){
editHistory.setVisibility(View.VISIBLE);
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));

View File

@@ -133,8 +133,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&

View File

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

View File

@@ -105,6 +105,21 @@ public abstract class StatusDisplayItem{
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
}
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
String parentID = parent.getID();
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
: fragment.getString(R.string.in_reply_to, account.displayName);
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: fragment.getString(R.string.in_reply_to, account.displayName);
return new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -120,17 +135,7 @@ public abstract class StatusDisplayItem{
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
: fragment.getString(R.string.in_reply_to, account.displayName);
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply)
: fragment.getString(R.string.in_reply_to, account.displayName);
replyLine = new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
}
if(status.reblog!=null){

View File

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

View File

@@ -56,6 +56,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
@@ -1052,50 +1053,46 @@ public class UiUtils {
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
);
}
public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
);
}
public static <T extends Searchable> void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
public static <T extends Searchable> Optional<MastodonAPIRequest<SearchResults>> lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
resultConsumer.accept(query);
return;
return Optional.empty();
}
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
.exec(targetAccountID));
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
@@ -1110,11 +1107,35 @@ public class UiUtils {
}
}
private static Bundle bundleError(String error) {
Bundle args = new Bundle();
args.putString("error", error);
return args;
}
private static Bundle bundleError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
return args;
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> {
if (clazz == null) return;
lookupURL(context, accountID, url, (clazz, args) -> {
if (clazz == null) {
if (args != null && args.containsKey("error")) Toast.makeText(context, args.getString("error"), Toast.LENGTH_SHORT).show();
if (launchBrowser) launchWebBrowser(context, url);
return;
}
Nav.go((Activity) context, clazz, args);
});
}).map(req -> req.wrapProgress((Activity) context, R.string.loading, true, d ->
transformDialogForLookup(context, accountID, url, d)));
}
public static boolean acctMatches(String accountID, String acct, String queriedUsername, @Nullable String queriedDomain) {
@@ -1137,15 +1158,17 @@ public class UiUtils {
}
}
public static void lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
parseFediverseHandle(query).ifPresentOrElse(
handle -> lookupAccountHandle(context, accountID, handle, go),
() -> go.accept(null, null)
);
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
return parseFediverseHandle(query).map(
handle -> lookupAccountHandle(context, accountID, handle, go))
.or(() -> {
go.accept(null, null);
return Optional.empty();
});
}
public static void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
public static MastodonAPIRequest<SearchResults> lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1159,29 +1182,22 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
args.putString("error", context.getString(R.string.sk_resource_not_found));
go.accept(null, null);
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
go.accept(null, args);
go.accept(null, bundleError(error));
}
}).exec(accountID);
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
Uri uri = Uri.parse(url);
List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1))
return Optional.of(new GetStatusByID(path.get(1))
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
@@ -1193,17 +1209,12 @@ public class UiUtils {
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
} else if (looksLikeFediverseUrl(url)) {
new GetSearchResults(url, null, true)
return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1221,26 +1232,19 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
}
}
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
return Optional.empty();
}
public static void copyText(View v, String text) {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M3.28 2.22c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.767 0 1.06l4.804 4.805-3.867 0.561C2.05 8.807 1.607 10.168 2.41 10.95l3.815 3.719-0.9 5.251c-0.19 1.103 0.968 1.944 1.958 1.423l4.716-2.479 4.716 2.48c0.99 0.52 2.148-0.32 1.96-1.424l-0.04-0.223 2.085 2.084c0.293 0.293 0.768 0.293 1.061 0 0.293-0.292 0.293-0.767 0-1.06L3.28 2.22zm13.518 15.639l0.345 2.014-4.516-2.374c-0.394-0.207-0.864-0.207-1.257 0l-4.516 2.374 0.862-5.03c0.075-0.437-0.07-0.884-0.388-1.194l-3.654-3.562 4.673-0.679 8.45 8.45zm3.525-7.772l-3.572 3.482 1.06 1.06 3.777-3.68c0.8-0.781 0.359-2.142-0.748-2.303L15.567 7.88l-2.358-4.777c-0.495-1.004-1.926-1.004-2.421 0L9.3 6.117l1.12 1.12 1.578-3.2 2.259 4.577c0.196 0.398 0.577 0.674 1.016 0.738l5.05 0.734z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/auto_reveal_never"
android:title="@string/sk_settings_auto_reveal_nobody" />
<item
android:id="@+id/auto_reveal_threads"
android:title="@string/sk_settings_auto_reveal_author" />
<item
android:id="@+id/auto_reveal_discussions"
android:title="@string/sk_settings_auto_reveal_anyone" />
</menu>

View File

@@ -294,6 +294,10 @@
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
<string name="sk_error_loading_profile">Konnte das Profil auf deiner Heim-Instanz nicht laden.</string>
<string name="sk_error_loading_profile">Konnte das Profil via %s nicht laden</string>
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower*innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Zeigen von gleichen CWs in Antworten von</string>
<string name="sk_settings_auto_reveal_nobody">niemandem</string>
<string name="sk_settings_auto_reveal_author">Autor*in</string>
<string name="sk_settings_auto_reveal_anyone">allen</string>
</resources>

View File

@@ -286,10 +286,19 @@
<string name="sk_settings_default_content_type">Contenido por defecto</string>
<string name="sk_settings_content_types_explanation">Permite establecer un tipo de contenido como Markdown al crear una entrada. Ten en cuenta que no todas las instancias lo admiten.</string>
<string name="sk_settings_default_content_type_explanation">Permite preseleccionar un tipo de contenido al crear nuevas entradas, anulando el valor establecido en \"Preferencias de publicación\".</string>
<string name="sk_bubble_timeline_info_banner">Estas son las publicaciones más recientes de la gente en tu servidor de Akkoma.</string>
<string name="sk_bubble_timeline_info_banner">Estas son las publicaciones más recientes de la red seleccionadas por los administradores de tu instancia.</string>
<string name="sk_timeline_bubble">Burbuja</string>
<string name="sk_instance_info_unavailable">Información de la instancia temporalmente no disponible</string>
<string name="sk_external_share_or_open_title">Compartir o abrir con una cuenta</string>
<string name="sk_open_in_app">Abrir en la app</string>
<string name="sk_external_share_title">Compartir con una cuenta</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Mostrar CW iguales en las respuestas de</string>
<string name="sk_settings_auto_reveal_nobody">nadie</string>
<string name="sk_settings_auto_reveal_author">autor</string>
<string name="sk_settings_auto_reveal_anyone">todos</string>
<string name="sk_open_in_app_failed">No se pudo abrir en la aplicación</string>
<string name="sk_no_remote_info_hint">no hay información remota disponible</string>
<string name="sk_error_loading_profile">No se pudo cargar el perfil a través de %s</string>
<string name="sk_settings_allow_remote_loading">Cargar la información desde las instancias remotas</string>
<string name="sk_settings_allow_remote_loading_explanation">Intenta obtener listas más precisas de seguidores, Me gusta y promociones cargando la información desde la instancia de origen.</string>
</resources>

View File

@@ -294,4 +294,8 @@
<string name="sk_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
<string name="sk_open_in_app_failed">Impossible de l\'ouvrir dans l\'application</string>
<string name="sk_settings_allow_remote_loading">Charger des informations à partir d\'instances distantes</string>
<string name="sk_no_remote_info_hint">informations distantes indisponibles</string>
<string name="sk_error_loading_profile">Échec du chargement du profil via %s</string>
<string name="sk_settings_allow_remote_loading_explanation">Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
</resources>

View File

@@ -289,8 +289,13 @@
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yamg paling terkini oleh orang-orang dalam gelembung server Akkoma Anda.</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yang paling terkini dari jaringan dikurasikan oleh admin server Anda.</string>
<string name="sk_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</string>
<string name="sk_external_share_or_open_title">Bagikan atau buka dengan akun</string>
<string name="sk_no_remote_info_hint">info jarak jauh tidak tersedia</string>
<string name="sk_settings_allow_remote_loading">Muat info dari server jarak jauh</string>
<string name="sk_settings_allow_remote_loading_explanation">Coba mendapatkan pendaftaran akurat untuk pengikut cr</string>
<string name="sk_error_loading_profile">Gagal memuat profil melalui %s</string>
<string name="sk_open_in_app_failed">Tidak dapat buka dalam aplikasi</string>
</resources>

View File

@@ -274,4 +274,26 @@
<string name="sk_compact_reblog_reply_line">Linea boost/risposta compatta</string>
<string name="sk_reacted_with">ha reagito con %s</string>
<string name="sk_reply_line_above_avatar">Linea \"In risposta a\" sopra l\'avatar</string>
<string name="sk_bubble_timeline_info_banner">Questi sono i post più recenti della rete, curati dagli amministratori della tua istanza.</string>
<string name="sk_settings_content_types_explanation">Permette di impostare un tipo di contenuto, come Markdown, quando si crea un post. Tieni presente che non tutte le istanze lo supportano.</string>
<string name="sk_open_in_app_failed">Impossibile aprire nell\'app</string>
<string name="sk_external_share_title">Condividi con l\'account</string>
<string name="sk_external_share_or_open_title">Condividi o apri con l\'account</string>
<string name="sk_no_remote_info_hint">Informazioni remote non disponibili</string>
<string name="sk_error_loading_profile">Impossibile caricare il profilo tramite %s</string>
<string name="sk_settings_allow_remote_loading">Carica informazioni da istanze remote</string>
<string name="sk_settings_allow_remote_loading_explanation">Cerca di ottenere elenchi più accurati di follower, like e boost caricando le informazioni dall\'istanza di origine.</string>
<string name="sk_content_type">Tipo di contenuto</string>
<string name="sk_timeline_bubble">Bolla</string>
<string name="sk_content_type_unspecified">Non specificato</string>
<string name="sk_content_type_plain">Testo semplice</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">Abilita la formattazione dei post</string>
<string name="sk_settings_default_content_type">Tipo di contenuto predefinito</string>
<string name="sk_settings_default_content_type_explanation">Ti permette di preselezionare un tipo di contenuto quando si creano nuovi post, sovrascrivendo il valore impostato in \"Preferenze di pubblicazione\".</string>
<string name="sk_instance_info_unavailable">Informazioni sull\'istanza temporaneamente non disponibili</string>
<string name="sk_open_in_app">Apri nell\'app</string>
</resources>

View File

@@ -269,6 +269,7 @@
<string name="hide_content">Dölj innehåll</string>
<string name="new_post">Nytt inlägg</string>
<string name="button_reply">Svara</string>
<string name="button_reblog">Boosta</string>
<string name="button_favorite">Favoritmarkera</string>
<string name="button_share">Dela</string>
<string name="media_no_description">Media utan beskrivning</string>
@@ -428,5 +429,7 @@
<string name="signup_or_login">eller</string>
<string name="learn_more">Läs mer</string>
<string name="welcome_to_mastodon">Välkommen till Mastodon</string>
<string name="welcome_paragraph1">Mastodon är ett decentraliserat socialt nätverk, vilket innebär att inget enskilt företag kontrollerar det. Det består av många oberoende servrar, alla sammankopplade.</string>
<string name="what_are_servers">Vad är servrar?</string>
<string name="welcome_paragraph2"><![CDATA[Varje Mastodon-konto finns på en server — var och en med sina värderingar, regler och administratörer. Oavsett vilken du väljer kan du följa och interagera med människor på vilken server som helst.]]></string>
</resources>

View File

@@ -293,4 +293,8 @@
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
<string name="sk_open_in_app_failed">Не вдалося відкрити в застосунку</string>
<string name="sk_no_remote_info_hint">віддалена інформація недоступна</string>
<string name="sk_settings_allow_remote_loading">Завантажити інформацію з віддалених серверів</string>
<string name="sk_settings_allow_remote_loading_explanation">Спробуйте отримати точніші списки підписників, вподобань і поширень, завантаживши інформацію з джерела.</string>
<string name="sk_error_loading_profile">Не вдалося завантажити профіль на ваш домашній сервер.</string>
</resources>

View File

@@ -294,7 +294,11 @@
<string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string>
<string name="sk_no_remote_info_hint">remote info unavailable</string>
<string name="sk_error_loading_profile">Failed loading the profile on your home instance.</string>
<string name="sk_error_loading_profile">Failed loading the profile via %s</string>
<string name="sk_settings_allow_remote_loading">Load info from remote instances</string>
<string name="sk_settings_allow_remote_loading_explanation">Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Reveal same CWs in replies from</string>
<string name="sk_settings_auto_reveal_nobody">nobody</string>
<string name="sk_settings_auto_reveal_author">author</string>
<string name="sk_settings_auto_reveal_anyone">everyone</string>
</resources>