Compare commits

..

97 Commits

Author SHA1 Message Date
sk
aa96ec54a3 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:09:39 +02:00
sk
e8b43c7179 preserve visibility when re-drafting 2022-05-26 22:09:02 +02:00
sk
b51b4a10ee Merge branch 'feature/delete-redraft' into fork 2022-05-26 21:44:07 +02:00
sk
f2b9ede27c Add proguard rules for parceler
according to http://parceler.org/
fixes issue where parceler can't find parcelable class
2022-05-26 21:43:39 +02:00
sk
a8c7d891f1 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:45:52 +02:00
sk
195c4d7b6d remove unused imports 2022-05-26 19:45:10 +02:00
sk
d280dc31e8 bump version 2022-05-26 19:35:36 +02:00
sk
eb0925c524 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:30:52 +02:00
sk
968de3664d fix german strings 2022-05-26 19:30:23 +02:00
sk
12f7336392 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:28:09 +02:00
sk
3a4d13b1c6 implement deleting and re-drafting 2022-05-26 19:19:42 +02:00
sk
273c841d9a Merge branch 'master' into feature/delete-redraft 2022-05-26 15:35:12 +02:00
sk
0186b7f8da Merge remote-tracking branch 'origin/fork' into fork 2022-05-22 02:14:06 +02:00
sk
d33654c793 bump version 2022-05-22 02:08:01 +02:00
Samuel Kaiser
86d2312615 Update README.md 2022-05-22 02:07:07 +02:00
sk
d1083c331b Merge branch 'feature/bookmarks' into fork 2022-05-22 02:04:08 +02:00
sk
ed7242217a add missing icon 2022-05-22 02:03:54 +02:00
sk
8fddaa8c82 implement bookmarks list!! 2022-05-22 02:03:29 +02:00
sk
00affe6e3e update readme 2022-05-21 23:38:56 +02:00
sk
f21b647ee0 bump version 2022-05-21 23:35:10 +02:00
sk
2a628a3791 Merge branch 'feature/back-returns-home' into fork 2022-05-21 23:34:18 +02:00
sk
ecd568503d make back button return home before exiting 2022-05-21 23:33:53 +02:00
sk
f9d0632a85 add missing files 2022-05-21 19:42:29 +02:00
sk
11905513b7 add missing icons 2022-05-21 19:40:49 +02:00
sk
9c89abf1c4 implement bookmark button 2022-05-21 19:27:44 +02:00
sk
4d950e43ac update readme 2022-05-21 18:59:13 +02:00
sk
99405f307d bump version 2022-05-21 18:49:59 +02:00
sk
f1bfe05263 Merge branch 'feature/always-preserve-cw' into fork 2022-05-21 18:49:07 +02:00
sk
0f223159c0 always preserve cw when replying 2022-05-21 18:48:48 +02:00
sk
ad9518e87c re-add deleted strings 2022-05-21 18:08:14 +02:00
sk
1c16cfb09e bump version 2022-05-21 18:05:01 +02:00
sk
d4a4b10017 Merge branch 'feature/compose-image-description-full-image' into fork 2022-05-21 18:03:29 +02:00
sk
74ae5bd04e change app name 2022-05-21 18:03:15 +02:00
sk
9638cf079f set image view height to wrap_content 2022-05-21 17:48:53 +02:00
sk
a6d161c1b4 minor code style change
for grishka's code style must prevail
2022-05-21 17:42:40 +02:00
sk
1136e40eb4 obey image max width 2022-05-21 17:39:28 +02:00
sk
98de3a2984 don't crop image when composing alt text 2022-05-21 17:27:31 +02:00
Grishka
080a320e12 Make the app name non-translatable 2022-05-17 18:47:11 +03:00
sk
b08415ca8f bump version 2022-05-15 20:35:10 +02:00
sk
3639c69d36 Merge branch 'master' into fork 2022-05-15 20:34:14 +02:00
Grishka
37cefcaf6d Fix #164 2022-05-15 21:13:36 +03:00
Grishka
558adc6936 Add compose shortcut
closes #131
2022-05-15 19:14:24 +03:00
sk
31e3a8592f bump version 2022-05-14 13:49:49 +02:00
sk
39655d5278 Merge branch 'master' into fork 2022-05-14 13:48:03 +02:00
Grishka
68d0862008 Close #122 2022-05-13 20:54:22 +03:00
Grishka
c9e13eefa5 Close #146 2022-05-13 20:49:35 +03:00
Grishka
349fbce5af Fix #128 2022-05-13 20:42:54 +03:00
Grishka
95c66654aa Fix #149 2022-05-13 19:20:40 +03:00
Grishka
a8407571a4 Fix #151 2022-05-13 19:18:29 +03:00
Grishka
75538deb9b Fix #156 2022-05-13 19:10:27 +03:00
Grishka
601eec4607 Fix #157 2022-05-13 19:01:29 +03:00
Grishka
9b87d0bece Fix #153 2022-05-13 18:14:52 +03:00
Grishka
cb25632691 Delete statuses from cache and fix auto-refresh when posting 2022-05-13 17:57:41 +03:00
Grishka
63957250c5 Fix #141 + crash fixes 2022-05-13 17:51:28 +03:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
sk
bde2e398a8 bump version and change app name 2022-05-06 22:10:45 +02:00
sk
8d443b2051 Merge branch 'feature/pin-posts' into fork 2022-05-06 21:50:08 +02:00
sk
33d4b678ed update posts' pinned states 2022-05-06 21:49:33 +02:00
sk
3becad1468 fix created posts being added to pinned 2022-05-06 21:18:48 +02:00
sk
fad3ba3eae bump version 2022-05-06 19:49:36 +02:00
sk
cb16f95878 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:45:33 +02:00
sk
4e833490ff fix about section not being displayed 2022-05-06 19:45:09 +02:00
Samuel Kaiser
04a973f7b0 Update README.md 2022-05-06 19:28:43 +02:00
sk
0318169b74 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:24:58 +02:00
sk
972fb1e241 translate "pinned" strings to german 2022-05-06 19:21:39 +02:00
sk
9beb04b01d implement pinning and unpinning posts 2022-05-06 19:07:51 +02:00
sk
a3bea6ad24 add profile tab for pinned toots 2022-05-06 18:09:00 +02:00
sk
7996e4ee4a change client name, versioning 2022-05-06 15:39:57 +02:00
sk
69c4bf4213 Merge branch 'feature/display-alt-text' into fork 2022-05-06 01:00:34 +02:00
sk
7cd5ca77f5 Merge remote-tracking branch 'origin/feature/display-alt-text' into feature/display-alt-text 2022-05-06 01:00:01 +02:00
sk
7e736d3cd3 clean up code 2022-05-06 00:59:43 +02:00
sk
13c2adba56 update version and readme 2022-05-06 00:34:08 +02:00
sk
010095a50e Merge branch 'master' into fork 2022-05-06 00:30:23 +02:00
Samuel Kaiser
f0cef2103f Merge branch 'mastodon:master' into feature/display-alt-text 2022-05-06 00:29:05 +02:00
sk
8ed731a48b edit application id for fork 2022-05-06 00:27:59 +02:00
sk
8660d43cb1 bump version 2022-05-06 00:21:08 +02:00
sk
0f495f620a Merge branch 'feature/display-alt-text' into fork 2022-05-06 00:17:07 +02:00
sk
ac81f10ea8 make button disappear when no description 2022-05-06 00:16:25 +02:00
sk
9aa95413e6 make text selectable 2022-05-06 00:15:56 +02:00
sk
a0a28a0cb7 implement scroll-to-close 2022-05-05 23:39:24 +02:00
sk
11d88aed27 implement alt text as bottom sheet 2022-05-05 23:26:48 +02:00
Grishka
88504531d4 Crash fixes 2022-05-05 22:05:18 +03:00
sk
899c9cdf21 implement alt text as toast messages 2022-05-05 19:28:27 +02:00
Grishka
4ad9fa030b Fix #127 2022-05-05 18:30:40 +03:00
sk
919d5cffb5 Merge branch 'master' into fork 2022-05-05 16:06:28 +02:00
Grishka
b4219bcaa0 Crash fix 2022-05-05 06:05:50 +03:00
sk
12599db0ff change dev versioning 2022-05-02 23:00:55 +02:00
sk
c751c85c1c add version number for upstream changes 2022-05-02 22:43:05 +02:00
sk
f1331a0f6d start fork versioning 2022-05-02 21:35:51 +02:00
sk
c75c9b60f9 add fork readme 2022-05-02 21:35:42 +02:00
sk
eb3adf1dfd Merge branch 'feature/add-federated-timeline' into fork 2022-05-02 21:35:14 +02:00
sk
6533163fd0 Merge branch 'master' into fork 2022-05-02 21:17:04 +02:00
sk
1becad6016 Merge branch 'feature/enable-unlisted-as-default' into fork 2022-05-02 19:32:26 +02:00
sk
d34653750e set unlisted as default visibility 2022-05-02 19:31:43 +02:00
sk
705592aefd set unlisted as default 2022-05-02 19:17:25 +02:00
sk
583325d6e8 add unlisted visibility option 2022-05-02 19:16:53 +02:00
sk
318d271127 add federation tab and change tab order 2022-05-02 18:31:29 +02:00
62 changed files with 1114 additions and 222 deletions

View File

@@ -1,11 +1,22 @@
# Mastodon for Android # Forked Mastodon for Android
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a> This is the repository for an officially forked Android app for Mastodon.
This is the repository for the official Android app for Mastodon. Learn more about the official app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/). ## Changes
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/tree/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/tree/feature/bookmarks) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/tree/feature/delete-redraft) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/21))
## Building ## Building
@@ -17,4 +28,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
## License ## License
This project is released under the [GPL-3 License](./LICENSE). This project is released under the [GPL-3 License](./LICENSE).

View File

@@ -6,11 +6,11 @@ plugins {
android { android {
compileSdk 31 compileSdk 31
defaultConfig { defaultConfig {
applicationId "org.joinmastodon.android" applicationId "org.joinmastodon.android.sk"
minSdk 23 minSdk 23
targetSdk 31 targetSdk 31
versionCode 36 versionCode 15
versionName "1.1.0" versionName '1.1.1+fork.15'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -46,4 +46,9 @@
-keep class org.joinmastodon.android.AppCenterWrapper { *; } -keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable -keepattributes LineNumberTable
# Parceler library
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }

View File

@@ -9,6 +9,7 @@ import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment; import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.SplashFragment; import org.joinmastodon.android.fragments.SplashFragment;
@@ -56,6 +57,8 @@ public class MainActivity extends FragmentStackActivity{
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){ if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification")); Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID()); showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
} }
} }
} }
@@ -91,6 +94,8 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args); fragment.setArguments(args);
showFragmentClearingBackStack(fragment); showFragmentClearingBackStack(fragment);
} }
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
} }
} }
@@ -115,4 +120,15 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args); fragment.setArguments(args);
showFragment(fragment); showFragment(fragment);
} }
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
return;
ComposeFragment compose=new ComposeFragment();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
compose.setArguments(composeArgs);
showFragment(compose);
}
} }

View File

@@ -233,6 +233,12 @@ public class CacheController{
}); });
} }
public void deleteStatus(String id){
runOnDbThread((db)->{
db.delete("home_timeline", "`id`=?", new String[]{id});
});
}
public void clearRecentSearches(){ public void clearRecentSearches(){
runOnDbThread((db)->db.delete("recent_searches", null, null)); runOnDbThread((db)->db.delete("recent_searches", null, null));
} }

View File

@@ -26,6 +26,8 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override @Override
public void showToast(Context context){ public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show(); Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
} }
} }

View File

@@ -4,6 +4,7 @@ import android.os.Looper;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited; import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -18,6 +19,7 @@ public class StatusInteractionController{
private final String accountID; private final String accountID;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>(); private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>(); private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID){ public StatusInteractionController(String accountID){
this.accountID=accountID; this.accountID=accountID;
@@ -61,6 +63,36 @@ public class StatusInteractionController{
E.post(new StatusCountersUpdatedEvent(status)); E.post(new StatusCountersUpdatedEvent(status));
} }
public void setBookmarked(Status status, boolean bookmarked){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
if(current!=null){
current.cancel();
}
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
runningBookmarkRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged){ public void setReblogged(Status status, boolean reblogged){
if(!Looper.getMainLooper().isCurrentThread()) if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread"); throw new IllegalStateException("Can only be called from main thread");

View File

@@ -21,6 +21,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){ switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true"); case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {} case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true"); case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> { case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true"); addQueryParameter("exclude_replies", "true");
@@ -32,6 +33,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
public enum Filter{ public enum Filter{
DEFAULT, DEFAULT,
INCLUDE_REPLIES, INCLUDE_REPLIES,
PINNED,
MEDIA, MEDIA,
NO_REBLOGS NO_REBLOGS
} }

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android.api.requests.accounts;
import androidx.annotation.NonNull;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okhttp3.Response;
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
private String maxId;
public GetBookmarks(String maxID, String minID, int limit){
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
super.validateAndPostprocessResponse(respObj, httpResponse);
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
String link=httpResponse.header("link");
// parsing link header by hand; using a library would be cleaner
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
if(link==null) return;
String maxIdEq="max_id=";
for(String s : link.split(",")) {
if(s.contains("rel=\"next\"")) {
int start=s.indexOf(maxIdEq)+maxIdEq.length();
int end=s.indexOf('>');
if(start<0 || start>end) return;
this.maxId=s.substring(start, end);
}
}
}
public String getMaxId() {
return maxId;
}
}

View File

@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
} }
private static class Request{ private static class Request{
public String clientName="Mastodon for Android"; public String clientName="Mastadon for Android";
public String redirectUris=AccountSessionManager.REDIRECT_URI; public String redirectUris=AccountSessionManager.REDIRECT_URI;
public String scopes=AccountSessionManager.SCOPE; public String scopes=AccountSessionManager.SCOPE;
public String website="https://app.joinmastodon.org/android"; public String website="https://github.com/sk22/mastodon-android-fork";
} }
} }

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusBookmarked extends MastodonAPIRequest<Status>{
public SetStatusBookmarked(String id, boolean bookmarked){
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusPinned extends MastodonAPIRequest<Status>{
public SetStatusPinned(String id, boolean pinned){
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
@@ -85,11 +92,12 @@ public class AccountSessionManager{
domains.add(session.domain.toLowerCase()); domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session); sessions.put(session.getID(), session);
} }
}catch(IOException|JsonParseException x){ }catch(Exception x){
Log.e(TAG, "Error loading accounts", x); Log.e(TAG, "Error loading accounts", x);
} }
lastActiveAccountID=prefs.getString("lastActiveAccount", null); lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains)); MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
maybeUpdateShortcuts();
} }
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){ public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
@@ -102,6 +110,7 @@ public class AccountSessionManager{
if(PushSubscriptionManager.arePushNotificationsAvailable()){ if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null); session.getPushSubscriptionManager().registerAccountForPush(null);
} }
maybeUpdateShortcuts();
} }
public synchronized void writeAccountsFile(){ public synchronized void writeAccountsFile(){
@@ -181,6 +190,7 @@ public class AccountSessionManager{
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class); NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(id); nm.deleteNotificationChannelGroup(id);
} }
maybeUpdateShortcuts();
} }
@NonNull @NonNull
@@ -358,7 +368,7 @@ public class AccountSessionManager{
customEmojis.put(domain, groupCustomEmojis(emojis)); customEmojis.put(domain, groupCustomEmojis(emojis));
instances.put(domain, emojis.instance); instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated); instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){ }catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x); Log.w(TAG, "Error reading instance info file for "+domain, x);
} }
} }
@@ -395,6 +405,29 @@ public class AccountSessionManager{
writeAccountsFile(); writeAccountsFile();
} }
private void maybeUpdateShortcuts(){
if(Build.VERSION.SDK_INT<26)
return;
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
}else if(sessions.isEmpty()){
// There are shortcuts, but no accounts. Disable existing shortcuts.
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
}
}
private static class SessionsStorageWrapper{ private static class SessionsStorageWrapper{
public List<AccountSession> accounts; public List<AccountSession> accounts;
} }

View File

@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
public class StatusCountersUpdatedEvent{ public class StatusCountersUpdatedEvent{
public String id; public String id;
public int favorites, reblogs, replies; public int favorites, reblogs, replies;
public boolean favorited, reblogged; public boolean favorited, reblogged, pinned;
public StatusCountersUpdatedEvent(Status s){ public StatusCountersUpdatedEvent(Status s){
id=s.id; id=s.id;
@@ -14,5 +14,6 @@ public class StatusCountersUpdatedEvent{
replies=s.repliesCount; replies=s.repliesCount;
favorited=s.favourited; favorited=s.favourited;
reblogged=s.reblogged; reblogged=s.reblogged;
pinned=s.pinned;
} }
} }

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class StatusUnpinnedEvent {
public final String id;
public final String accountID;
public StatusUnpinnedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onStatusCreated(StatusCreatedEvent ev){ protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account)) if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
return; return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){ if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies // Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id)) if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
} }
prependItems(Collections.singletonList(ev.status), true); prependItems(Collections.singletonList(ev.status), true);
} }
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
return;
Status status=getStatusByID(ev.id);
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
} }

View File

@@ -461,9 +461,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null) if(header!=null)
header.rebind(); header.rebind();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){ updateImagesSpoilerState(status, itemID);
photo.setRevealed(true);
}
} }
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){ public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
@@ -472,12 +470,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!TextUtils.isEmpty(status.spoilerText)){ if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class); TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){ if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset()); adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
} }
} }
holder.rebind(); holder.rebind();
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){ updateImagesSpoilerState(status, holder.getItemID());
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(status.spoilerRevealed); photo.setRevealed(status.spoilerRevealed);
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
} }
} }

View File

@@ -0,0 +1,53 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class BookmarksListFragment extends StatusListFragment{
private String accountID;
private Account self;
private String lastMaxId=null;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
setTitle(R.string.bookmarks);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count) {
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
currentRequest=b.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, b.getMaxId()!=null);
lastMaxId=b.getMaxId();
}
})
.exec(accountID);
}
}

View File

@@ -22,6 +22,7 @@ import android.text.Layout;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.format.DateUtils;
import android.util.Log; import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -167,13 +168,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageView sendError; private ImageView sendError;
private View sendingOverlay; private View sendingOverlay;
private WindowManager wm; private WindowManager wm;
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC; private StatusPrivacy statusVisibility=StatusPrivacy.UNLISTED;
private ComposeAutocompleteSpan currentAutocompleteSpan; private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap; private FrameLayout mainEditTextWrap;
private ComposeAutocompleteViewController autocompleteViewController; private ComposeAutocompleteViewController autocompleteViewController;
private Instance instance; private Instance instance;
private boolean attachmentsErrorShowing; private boolean attachmentsErrorShowing;
public static DraftMediaAttachment redraftAttachment(Attachment att) {
DraftMediaAttachment draft=new DraftMediaAttachment();
draft.serverAttachment=att;
draft.description=att.description;
draft.uri=Uri.parse(att.url);
return draft;
}
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -204,6 +213,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo")); replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility=replyTo.visibility; statusVisibility=replyTo.visibility;
} }
if(getArguments().containsKey("visibility")){
statusVisibility=(StatusPrivacy) getArguments().getSerializable("visibility");
}
if(savedInstanceState!=null){ if(savedInstanceState!=null){
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility"); statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
} }
@@ -286,11 +300,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollDurationView.setOnClickListener(v->showPollDurationMenu()); pollDurationView.setOnClickListener(v->showPollDurationMenu());
pollOptions.clear(); pollOptions.clear();
if(savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){ ArrayList<String> restoredPollOptions=(savedInstanceState!=null ? savedInstanceState : getArguments())
.getStringArrayList("pollOptions");
if(restoredPollOptions!=null){
if(savedInstanceState==null){
// restoring from arguments
pollDuration=getArguments().getInt("pollDuration");
pollDurationStr=DateUtils.formatElapsedTime(pollDuration); // getResources().getQuantityString(R.plurals.x_hours, pollDuration/3600);
}
pollBtn.setSelected(true); pollBtn.setSelected(true);
mediaBtn.setEnabled(false); mediaBtn.setEnabled(false);
pollWrap.setVisibility(View.VISIBLE); pollWrap.setVisibility(View.VISIBLE);
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){ for(String oldText:restoredPollOptions){
DraftPollOption opt=createDraftPollOption(); DraftPollOption opt=createDraftPollOption();
opt.edit.setText(oldText); opt.edit.setText(oldText);
} }
@@ -310,8 +331,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setSelected(true); spoilerBtn.setSelected(true);
} }
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){ ArrayList<Parcelable> serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments())
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments"); .getParcelableArrayList("attachments");
if(serializedAttachments!=null){
for(Parcelable a:serializedAttachments){ for(Parcelable a:serializedAttachments){
DraftMediaAttachment att=Parcels.unwrap(a); DraftMediaAttachment att=Parcels.unwrap(a);
attachmentsView.addView(createMediaAttachmentView(att)); attachmentsView.addView(createMediaAttachmentView(att));
@@ -453,11 +475,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(savedInstanceState==null){ if(savedInstanceState==null){
mainEditText.setText(initialText); mainEditText.setText(initialText);
mainEditText.setSelection(mainEditText.length()); mainEditText.setSelection(mainEditText.length());
if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){ // TODO: setting for preserving cw always / only when replying to own posts
// && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)
if(!TextUtils.isEmpty(replyTo.spoilerText)){
insertSpoiler(replyTo.spoilerText);
hasSpoiler=true; hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
spoilerEdit.setText(replyTo.spoilerText);
spoilerBtn.setSelected(true);
} }
} }
}else{ }else{
@@ -470,6 +492,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditText.setSelection(mainEditText.length()); mainEditText.setSelection(mainEditText.length());
initialText=prefilledText; initialText=prefilledText;
} }
String spoilerText=getArguments().getString("spoilerText");
if(!TextUtils.isEmpty(spoilerText)) insertSpoiler(spoilerText);
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments"); ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
if(mediaUris!=null && !mediaUris.isEmpty()){ if(mediaUris!=null && !mediaUris.isEmpty()){
for(Uri uri:mediaUris){ for(Uri uri:mediaUris){
@@ -479,6 +503,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
} }
private void insertSpoiler(String text) {
hasSpoiler=true;
if (text!=null) spoilerEdit.setText(text);
spoilerEdit.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true);
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=new Button(getActivity()); publishButton=new Button(getActivity());
@@ -547,8 +578,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(opt.edit.length()>0) if(opt.edit.length()>0)
nonEmptyPollOptionsCount++; nonEmptyPollOptionsCount++;
} }
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty() if(publishButton!=null){
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1)); publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit
&& uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
}
} }
private void onCustomEmojiClick(Emoji emoji){ private void onCustomEmojiClick(Emoji emoji){
@@ -609,17 +643,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result){
wm.removeView(sendingOverlay); wm.removeView(sendingOverlay);
Nav.finish(ComposeFragment.this); sendingOverlay=null;
E.post(new StatusCreatedEvent(result)); E.post(new StatusCreatedEvent(result));
if(replyTo!=null){ if(replyTo!=null){
replyTo.repliesCount++; replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo)); E.post(new StatusCountersUpdatedEvent(replyTo));
} }
Nav.finish(ComposeFragment.this);
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
wm.removeView(sendingOverlay); wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE); sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE); sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true); publishButton.setEnabled(true);
@@ -633,8 +669,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
boolean pollFieldsHaveContent=false; boolean pollFieldsHaveContent=false;
for(DraftPollOption opt:pollOptions) for(DraftPollOption opt:pollOptions)
pollFieldsHaveContent|=opt.edit.length()>0; pollFieldsHaveContent|=opt.edit.length()>0;
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty() return getArguments().getBoolean("hasDraft", false)
|| uploadingAttachment!=null || !queuedAttachments.isEmpty() || !failedAttachments.isEmpty() || pollFieldsHaveContent; || (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText))
|| !attachments.isEmpty() || uploadingAttachment!=null || !queuedAttachments.isEmpty()
|| !failedAttachments.isEmpty() || pollFieldsHaveContent;
} }
@Override @Override
@@ -647,6 +685,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
confirmDiscardDraftAndFinish(); confirmDiscardDraftAndFinish();
return true; return true;
} }
if(sendingOverlay!=null)
return true;
return false; return false;
} }
@@ -1029,7 +1069,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
UiUtils.enablePopupMenuIcons(getActivity(), menu); UiUtils.enablePopupMenuIcons(getActivity(), menu);
m.setGroupCheckable(0, true, true); m.setGroupCheckable(0, true, true);
m.findItem(switch(statusVisibility){ m.findItem(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.id.vis_public; case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers; case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private; case DIRECT -> R.id.vis_private;
}).setChecked(true); }).setChecked(true);
@@ -1039,6 +1080,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
int id=item.getItemId(); int id=item.getItemId();
if(id==R.id.vis_public){ if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC; statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_unlisted){
statusVisibility=StatusPrivacy.UNLISTED;
}else if(id==R.id.vis_followers){ }else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE; statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){ }else if(id==R.id.vis_private){

View File

@@ -255,9 +255,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override @Override
public boolean onBackPressed(){ public boolean onBackPressed(){
if(currentTab==R.id.tab_profile) if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed(); if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search) if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed(); if (searchFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
return true;
}
return false; return false;
} }

View File

@@ -161,7 +161,7 @@ public class HomeTimelineFragment extends StatusListFragment{
return; return;
Status last=result.get(result.size()-1); Status last=result.get(result.size()-1);
List<Status> toAdd; List<Status> toAdd;
if(last.id.equals(data.get(0).id)){ // This part intersects with the existing one if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one
toAdd=result.subList(0, result.size()-1); // Remove the already known last post toAdd=result.subList(0, result.size()-1); // Remove the already known last post
}else{ }else{
result.get(result.size()-1).hasGapAfter=true; result.get(result.size()-1).hasGapAfter=true;

View File

@@ -101,7 +101,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton; private ProgressBarButton actionButton;
private ViewPager2 pager; private ViewPager2 pager;
private NestedRecyclerScrollView scrollView; private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment; private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment; private ProfileAboutFragment aboutFragment;
private TabLayout tabbar; private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout; private SwipeRefreshLayout refreshLayout;
@@ -128,6 +128,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View fab; private View fab;
private WindowInsets childInsets; private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer; private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
public ProfileFragment(){ public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar); super(R.layout.loader_fragment_overlay_toolbar);
@@ -208,14 +209,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
} }
}; };
tabViews=new FrameLayout[4]; tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){ for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity()); FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){ tabView.setId(switch(i){
case 0 -> R.id.profile_posts; case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies; case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media; case 2 -> R.id.profile_pinned_posts;
case 3 -> R.id.profile_about; case 3 -> R.id.profile_media;
case 4 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i); default -> throw new IllegalStateException("Unexpected value: "+i);
}); });
tabView.setVisibility(View.GONE); tabView.setVisibility(View.GONE);
@@ -223,7 +225,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabViews[i]=tabView; tabViews[i]=tabView;
} }
pager.setOffscreenPageLimit(4); pager.setOffscreenPageLimit(5);
pager.setAdapter(new ProfilePagerAdapter()); pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels; pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -239,8 +241,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){ tab.setText(switch(position){
case 0 -> R.string.posts; case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies; case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media; case 2 -> R.string.pinned_posts;
case 3 -> R.string.profile_about; case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException(); default -> throw new IllegalStateException();
}); });
} }
@@ -297,6 +300,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsFragment.onRefresh(); postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded) if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh(); postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded) if(mediaFragment.loaded)
mediaFragment.onRefresh(); mediaFragment.onRefresh();
} }
@@ -321,6 +326,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){ if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true); postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false); postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment =AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false); mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment(); aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields); aboutFragment.setFields(fields);
@@ -401,6 +407,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){ if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets); postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets); postsWithRepliesFragment.onApplyWindowInsets(childInsets);
pinnedPostsFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets); mediaFragment.onApplyWindowInsets(childInsets);
} }
} }
@@ -508,10 +515,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return; return;
} }
if(relationship==null) if(relationship==null && !isOwnProfile)
return; return;
inflater.inflate(R.menu.profile, menu); inflater.inflate(R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername())); menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
if(isOwnProfile){
for(int i=0;i<menu.size();i++){
MenuItem item=menu.getItem(i);
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks);
}
return;
}
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername())); menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername())); menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername())); menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
@@ -528,11 +542,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId(); int id=item.getItemId();
if(id==R.id.share){ if(id==R.id.share) {
Intent intent=new Intent(Intent.ACTION_SEND); Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain"); intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url); intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle())); startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.bookmarks) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(getActivity(), BookmarksListFragment.class, args);
}else if(id==R.id.mute){ }else if(id==R.id.mute){
confirmToggleMuted(); confirmToggleMuted();
}else if(id==R.id.block){ }else if(id==R.id.block){
@@ -636,8 +655,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return switch(page){ return switch(page){
case 0 -> postsFragment; case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment; case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment; case 2 -> pinnedPostsFragment;
case 3 -> aboutFragment; case 3 -> mediaFragment;
case 4 -> aboutFragment;
default -> throw new IllegalStateException(); default -> throw new IllegalStateException();
}; };
} }
@@ -664,17 +684,26 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
} }
private void loadAccountInfoAndEnterEditMode(){ private void loadAccountInfoAndEnterEditMode(){
if(editModeLoading)
return;
editModeLoading=true;
setActionProgressVisible(true); setActionProgressVisible(true);
new GetOwnAccount() new GetOwnAccount()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Account result){ public void onSuccess(Account result){
editModeLoading=false;
if(getActivity()==null)
return;
enterEditMode(result); enterEditMode(result);
setActionProgressVisible(false); setActionProgressVisible(false);
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
editModeLoading=false;
if(getActivity()==null)
return;
error.showToast(getActivity()); error.showToast(getActivity());
setActionProgressVisible(false); setActionProgressVisible(false);
} }
@@ -689,9 +718,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu(); invalidateOptionsMenu();
pager.setUserInputEnabled(false); pager.setUserInputEnabled(false);
actionButton.setText(R.string.done); actionButton.setText(R.string.done);
pager.setCurrentItem(3); pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>(); ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<3;i++){ for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f)); animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false); tabbar.getTabAt(i).view.setEnabled(false);
} }
@@ -732,7 +761,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu(); invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>(); ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile); actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){ for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f)); animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
} }
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0)); animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
@@ -750,7 +779,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){ set.addListener(new AnimatorListenerAdapter(){
@Override @Override
public void onAnimationEnd(Animator animation){ public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){ for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true); tabbar.getTabAt(i).view.setEnabled(true);
} }
pager.setUserInputEnabled(true); pager.setUserInputEnabled(true);
@@ -927,7 +956,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override @Override
public int getItemCount(){ public int getItemCount(){
return loaded ? 4 : 0; return loaded ? tabViews.length : 0;
} }
@Override @Override

View File

@@ -13,6 +13,8 @@ public interface ScrollableToTop{
* @param list * @param list
*/ */
default void smoothScrollRecyclerViewToTop(RecyclerView list){ default void smoothScrollRecyclerViewToTop(RecyclerView list){
if(list==null) // TODO find out why this happens because it should not be possible
return;
if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){ if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){
list.scrollToPosition(0); list.scrollToPosition(0);
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){

View File

@@ -9,10 +9,10 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -61,6 +61,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onStatusCreated(StatusCreatedEvent ev){} protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){}
protected Status getContentStatusByID(String id){ protected Status getContentStatusByID(String id){
Status s=getStatusByID(id); Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus(); return s==null ? null : s.getContentStatus();
@@ -113,10 +115,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return; return;
data.remove(status); data.remove(status);
preloadedData.remove(status); preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class); int index=-1;
if(item==null) for(int i=0;i<displayItems.size();i++){
if(ev.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return; return;
int index=displayItems.indexOf(item);
int lastIndex; int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){ for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id)) if(!displayItems.get(lastIndex).parentID.equals(ev.id))
@@ -131,6 +138,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
StatusListFragment.this.onStatusCreated(ev); StatusListFragment.this.onStatusCreated(ev);
} }
@Subscribe
public void onStatusUnpinned(StatusUnpinnedEvent ev){
StatusListFragment.this.onStatusUnpinned(ev);
}
@Subscribe @Subscribe
public void onPollUpdated(PollUpdatedEvent ev){ public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID)) if(!ev.accountID.equals(accountID))

View File

@@ -22,6 +22,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
@@ -234,7 +235,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
public void bindRelationship(){ public void bindRelationship(){
Relationship rel=relationships.get(item.account.id); Relationship rel=relationships.get(item.account.id);
if(rel==null){ if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE); button.setVisibility(View.GONE);
}else{ }else{
button.setVisibility(View.VISIBLE); button.setVisibility(View.VISIBLE);

View File

@@ -51,6 +51,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private DiscoverAccountsFragment accountsFragment; private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment; private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment; private LocalTimelineFragment localTimelineFragment;
private FederatedTimelineFragment federatedTimelineFragment;
private String accountID; private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced; private Runnable searchDebouncer=this::onSearchChangedDebounced;
@@ -72,15 +73,16 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar); tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager); pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[5]; tabViews=new FrameLayout[6];
for(int i=0;i<tabViews.length;i++){ for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity()); FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){ tabView.setId(switch(i){
case 0 -> R.id.discover_posts; case 0 -> R.id.discover_local_timeline;
case 1 -> R.id.discover_hashtags; case 1 -> R.id.discover_federated_timeline;
case 2 -> R.id.discover_news; case 2 -> R.id.discover_hashtags;
case 3 -> R.id.discover_local_timeline; case 3 -> R.id.discover_posts;
case 4 -> R.id.discover_users; case 4 -> R.id.discover_news;
case 5 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i); default -> throw new IllegalStateException("Unexpected value: "+i);
}); });
tabView.setVisibility(View.GONE); tabView.setVisibility(View.GONE);
@@ -106,7 +108,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
} }
}); });
if(postsFragment==null){ if(localTimelineFragment==null){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putBoolean("__is_tab", true); args.putBoolean("__is_tab", true);
@@ -126,9 +128,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
localTimelineFragment=new LocalTimelineFragment(); localTimelineFragment=new LocalTimelineFragment();
localTimelineFragment.setArguments(args); localTimelineFragment.setArguments(args);
federatedTimelineFragment=new FederatedTimelineFragment();
federatedTimelineFragment.setArguments(args);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment) .add(R.id.discover_posts, postsFragment)
.add(R.id.discover_local_timeline, localTimelineFragment) .add(R.id.discover_local_timeline, localTimelineFragment)
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
.add(R.id.discover_hashtags, hashtagsFragment) .add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment) .add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment) .add(R.id.discover_users, accountsFragment)
@@ -139,17 +145,30 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
@Override @Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){ tab.setText(switch(position){
case 0 -> R.string.posts; case 0 -> R.string.local_timeline;
case 1 -> R.string.hashtags; case 1 -> R.string.federated_timeline;
case 2 -> R.string.news; case 2 -> R.string.hashtags;
case 3 -> R.string.local_timeline; case 3 -> R.string.posts;
case 4 -> R.string.for_you; case 4 -> R.string.news;
case 5 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position); default -> throw new IllegalStateException("Unexpected value: "+position);
}); });
tab.view.textView.setAllCaps(true); tab.view.textView.setAllCaps(true);
} }
}); });
tabLayoutMediator.attach(); tabLayoutMediator.attach();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
searchEdit=view.findViewById(R.id.search_edit); searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged); searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
@@ -217,8 +236,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
} }
public void loadData(){ public void loadData(){
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading) if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
postsFragment.loadData(); localTimelineFragment.loadData();
} }
private void onSearchEditFocusChanged(View v, boolean hasFocus){ private void onSearchEditFocusChanged(View v, boolean hasFocus){
@@ -254,11 +273,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private Fragment getFragmentForPage(int page){ private Fragment getFragmentForPage(int page){
return switch(page){ return switch(page){
case 0 -> postsFragment; case 0 -> localTimelineFragment;
case 1 -> hashtagsFragment; case 1 -> federatedTimelineFragment;
case 2 -> newsFragment; case 2 -> hashtagsFragment;
case 3 -> localTimelineFragment; case 3 -> postsFragment;
case 4 -> accountsFragment; case 4 -> newsFragment;
case 5 -> accountsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page); default -> throw new IllegalStateException("Unexpected value: "+page);
}; };
} }

View File

@@ -0,0 +1,41 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FederatedTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
private String maxID;
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
}

View File

@@ -117,6 +117,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
if(isInRecentMode()){ if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{ AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
return;
unfilteredResults=sr; unfilteredResults=sr;
prevDisplayItems=new ArrayList<>(displayItems); prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false); onDataLoaded(sr, false);
@@ -203,7 +205,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override @Override
public void onTabReselected(TabLayout.Tab tab){ public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
} }
}); });
} }

View File

@@ -126,6 +126,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
repliesCount=ev.replies; repliesCount=ev.replies;
favourited=ev.favorited; favourited=ev.favorited;
reblogged=ev.reblogged; reblogged=ev.reblogged;
pinned=ev.pinned;
} }
public Status getContentStatus(){ public Status getContentStatus(){

View File

@@ -162,6 +162,7 @@ public class ComposeAutocompleteViewController{
.map(WrappedEmoji::new) .map(WrappedEmoji::new)
.collect(Collectors.toList()); .collect(Collectors.toList());
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode)); UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
imgLoader.updateImages();
} }
} }
@@ -186,6 +187,7 @@ public class ComposeAutocompleteViewController{
List<WrappedAccount> oldList=users; List<WrappedAccount> oldList=users;
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList()); users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id)); UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
imgLoader.updateImages();
if(listIsHidden){ if(listIsHidden){
listIsHidden=false; listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE); V.setVisibilityAnimated(list, View.VISIBLE);
@@ -210,6 +212,7 @@ public class ComposeAutocompleteViewController{
List<Hashtag> oldList=hashtags; List<Hashtag> oldList=hashtags;
hashtags=result.hashtags; hashtags=result.hashtags;
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name)); UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
imgLoader.updateImages();
if(listIsHidden){ if(listIsHidden){
listIsHidden=false; listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE); V.setVisibilityAnimated(list, View.VISIBLE);

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.ui;
import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.UsableRecyclerView;
public class ImageDescriptionSheet extends BottomSheet{
private UsableRecyclerView list;
public ImageDescriptionSheet(@NonNull Activity activity, Attachment attachment){
super(activity);
View handleView=new View(activity);
handleView.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
ViewGroup handle=new FrameLayout(activity);
handle.addView(handleView);
handle.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
TextView textView = new TextView(activity);
if (attachment.description == null || attachment.description.isEmpty()) {
textView.setText(R.string.media_no_description);
textView.setTypeface(null, Typeface.ITALIC);
} else {
textView.setText(attachment.description);
textView.setTextIsSelectable(true);
}
TextView heading=new TextView(activity);
heading.setText(R.string.image_description);
heading.setAllCaps(true);
heading.setTypeface(null, Typeface.BOLD);
heading.setPadding(0, V.dp(24), 0, V.dp(8));
LinearLayout linearLayout = new LinearLayout(activity);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setPadding(V.dp(24), 0, V.dp(24), 0);
linearLayout.addView(heading);
linearLayout.addView(textView);
FrameLayout layout=new FrameLayout(activity);
layout.addView(handle);
layout.addView(linearLayout);
list=new UsableRecyclerView(activity);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setBackgroundResource(R.drawable.bg_bottom_sheet);
list.setAdapter(new SingleViewRecyclerAdapter(layout));
list.setClipToPadding(false);
setContentView(list);
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
}
@Override
protected void onWindowInsetsUpdated(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
int tappableBottom=insets.getTappableElementInsets().bottom;
int insetBottom=insets.getSystemWindowInsetBottom();
if(tappableBottom==0 && insetBottom>0){
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
}else{
list.setPadding(0, 0, 0, V.dp(24));
}
}else{
list.setPadding(0, 0, 0, V.dp(24));
}
}
}

View File

@@ -92,7 +92,9 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
public void onBind(AudioStatusDisplayItem item){ public void onBind(AudioStatusDisplayItem item){
int seconds=(int)item.attachment.getDuration(); int seconds=(int)item.attachment.getDuration();
String duration=formatDuration(seconds); String duration=formatDuration(seconds);
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration)); // Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
time.setText(duration); time.setText(duration);
AudioPlayerService service=AudioPlayerService.getInstance(); AudioPlayerService service=AudioPlayerService.getInstance();
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){ if(service!=null && service.getAttachmentID().equals(item.attachment.id)){

View File

@@ -43,7 +43,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{ public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite; private final TextView reply, boost, favorite, bookmark;
private final ImageView share; private final ImageView share;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){ private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@@ -60,22 +60,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
reply=findViewById(R.id.reply); reply=findViewById(R.id.reply);
boost=findViewById(R.id.boost); boost=findViewById(R.id.boost);
favorite=findViewById(R.id.favorite); favorite=findViewById(R.id.favorite);
bookmark=findViewById(R.id.bookmark);
share=findViewById(R.id.share); share=findViewById(R.id.share);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){ if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(reply); UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
UiUtils.fixCompoundDrawableTintOnAndroid6(boost); UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite); UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
} }
View reply=findViewById(R.id.reply_btn); View reply=findViewById(R.id.reply_btn);
View boost=findViewById(R.id.boost_btn); View boost=findViewById(R.id.boost_btn);
View favorite=findViewById(R.id.favorite_btn); View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn); View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnClickListener(this::onReplyClick); reply.setOnClickListener(this::onReplyClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate); reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnClickListener(this::onBoostClick); boost.setOnClickListener(this::onBoostClick);
boost.setAccessibilityDelegate(buttonAccessibilityDelegate); boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnClickListener(this::onFavoriteClick); favorite.setOnClickListener(this::onFavoriteClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate); favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnClickListener(this::onShareClick); share.setOnClickListener(this::onShareClick);
share.setAccessibilityDelegate(buttonAccessibilityDelegate); share.setAccessibilityDelegate(buttonAccessibilityDelegate);
} }
@@ -87,6 +92,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount); bindButton(favorite, item.status.favouritesCount);
boost.setSelected(item.status.reblogged); boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited); favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id))); || (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
} }
@@ -120,6 +126,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount); bindButton(favorite, item.status.favouritesCount);
} }
private void onBookmarkClick(View v){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
bookmark.setSelected(item.status.bookmarked);
}
private void onShareClick(View v){ private void onShareClick(View v){
Intent intent=new Intent(Intent.ACTION_SEND); Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain"); intent.setType("text/plain");
@@ -134,6 +145,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return R.string.button_reblog; return R.string.button_reblog;
if(id==R.id.favorite_btn) if(id==R.id.favorite_btn)
return R.string.button_favorite; return R.string.button_favorite;
if(id==R.id.bookmark_btn)
return R.string.button_bookmark;
if(id==R.id.share_btn) if(id==R.id.share_btn)
return R.string.button_share; return R.string.button_share;
return 0; return 0;

View File

@@ -137,6 +137,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
int id=menuItem.getItemId(); int id=menuItem.getItemId();
if(id==R.id.delete){ if(id==R.id.delete){
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}); UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.pin || id==R.id.unpin){
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
}else if(id==R.id.delete_and_redraft) {
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.mute){ }else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{}); UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
}else if(id==R.id.block){ }else if(id==R.id.block){
@@ -250,6 +254,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
Menu menu=optionsMenu.getMenu(); Menu menu=optionsMenu.getMenu();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account); boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null); menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
MenuItem blockDomain=menu.findItem(R.id.block_domain); MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute); MenuItem mute=menu.findItem(R.id.mute);

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.LinkedTextView;
@@ -20,7 +21,8 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class TextStatusDisplayItem extends StatusDisplayItem{ public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence text; private CharSequence text;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
private CharSequence parsedSpoilerText;
public boolean textSelectable; public boolean textSelectable;
public final Status status; public final Status status;
@@ -29,6 +31,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
this.text=text; this.text=text;
this.status=status; this.status=status;
emojiHelper.setText(text); emojiHelper.setText(text);
if(!TextUtils.isEmpty(status.spoilerText)){
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
spoilerEmojiHelper=new CustomEmojiHelper();
spoilerEmojiHelper.setText(parsedSpoilerText);
}
} }
@Override @Override
@@ -38,11 +45,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public int getImageCount(){ public int getImageCount(){
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
return spoilerEmojiHelper.getImageCount();
return emojiHelper.getImageCount(); return emojiHelper.getImageCount();
} }
@Override @Override
public ImageLoaderRequest getImageRequest(int index){ public ImageLoaderRequest getImageRequest(int index){
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
return spoilerEmojiHelper.getImageRequest(index);
return emojiHelper.getImageRequest(index); return emojiHelper.getImageRequest(index);
} }
@@ -65,7 +76,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable); text.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false); text.setInvalidateOnEveryFrame(false);
if(!TextUtils.isEmpty(item.status.spoilerText)){ if(!TextUtils.isEmpty(item.status.spoilerText)){
spoilerTitle.setText(item.status.spoilerText); spoilerTitle.setText(item.parsedSpoilerText);
if(item.status.spoilerRevealed){ if(item.status.spoilerRevealed){
spoilerOverlay.setVisibility(View.GONE); spoilerOverlay.setVisibility(View.GONE);
text.setVisibility(View.VISIBLE); text.setVisibility(View.VISIBLE);
@@ -84,8 +95,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void setImage(int index, Drawable image){ public void setImage(int index, Drawable image){
item.emojiHelper.setImageDrawable(index, image); getEmojiHelper().setImageDrawable(index, image);
text.invalidate(); text.invalidate();
spoilerTitle.invalidate();
if(image instanceof Animatable){ if(image instanceof Animatable){
((Animatable) image).start(); ((Animatable) image).start();
if(image instanceof MovieDrawable) if(image instanceof MovieDrawable)
@@ -95,8 +107,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void clearImage(int index){ public void clearImage(int index){
item.emojiHelper.setImageDrawable(index, null); getEmojiHelper().setImageDrawable(index, null);
text.invalidate(); text.invalidate();
} }
private CustomEmojiHelper getEmojiHelper(){
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
}
} }
} }

View File

@@ -19,6 +19,7 @@ import android.media.AudioManager;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import android.opengl.Visibility;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.SystemClock; import android.os.SystemClock;
@@ -48,6 +49,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.ImageDescriptionSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.io.File; import java.io.File;
@@ -97,6 +99,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
private TextView videoTimeView; private TextView videoTimeView;
private ImageButton videoPlayPauseButton; private ImageButton videoPlayPauseButton;
private View videoControls; private View videoControls;
private MenuItem imageDescriptionButton;
private boolean uiVisible=true; private boolean uiVisible=true;
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged; private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
private Runnable uiAutoHider=()->{ private Runnable uiAutoHider=()->{
@@ -174,11 +177,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap); toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
toolbar=uiOverlay.findViewById(R.id.toolbar); toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0)); toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); imageDescriptionButton = toolbar.getMenu()
toolbar.setOnMenuItemClickListener(item->{ .add(R.string.image_description)
saveCurrentFile(); .setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
return true; .setVisible(attachments.get(pager.getCurrentItem()).description != null
}); && !attachments.get(pager.getCurrentItem()).description.isEmpty())
.setOnMenuItemClickListener(item -> {
new ImageDescriptionSheet(activity,attachments.get(pager.getCurrentItem())).show();
return true;
});
imageDescriptionButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.getMenu()
.add(R.string.download)
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
.setOnMenuItemClickListener(item -> {
saveCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
uiOverlay.setAlpha(0f); uiOverlay.setAlpha(0f);
videoControls=uiOverlay.findViewById(R.id.video_player_controls); videoControls=uiOverlay.findViewById(R.id.video_player_controls);
videoSeekBar=uiOverlay.findViewById(R.id.seekbar); videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
@@ -374,6 +390,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
private void onPageChanged(int index){ private void onPageChanged(int index){
currentIndex=index; currentIndex=index;
Attachment att=attachments.get(index); Attachment att=attachments.get(index);
imageDescriptionButton.setVisible(att.description != null && !att.description.isEmpty());
toolbar.invalidate();
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE); V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
if(att.type==Attachment.Type.VIDEO){ if(att.type==Attachment.Type.VIDEO){
videoSeekBar.setSecondaryProgress(0); videoSeekBar.setSecondaryProgress(0);

View File

@@ -0,0 +1,8 @@
package org.joinmastodon.android.ui.text;
/**
* A span to mark character ranges that should be deleted when copied to the clipboard.
* Works with {@link org.joinmastodon.android.ui.views.LinkedTextView}.
*/
public class DeleteWhenCopiedSpan{
}

View File

@@ -67,10 +67,9 @@ public class HtmlParser{
@Override @Override
public void head(@NonNull Node node, int depth){ public void head(@NonNull Node node, int depth){
if(node instanceof TextNode){ if(node instanceof TextNode textNode){
ssb.append(((TextNode) node).text()); ssb.append(textNode.text());
}else if(node instanceof Element){ }else if(node instanceof Element el){
Element el=(Element)node;
switch(el.nodeName()){ switch(el.nodeName()){
case "a" -> { case "a" -> {
String href=el.attr("href"); String href=el.attr("href");
@@ -108,10 +107,9 @@ public class HtmlParser{
@Override @Override
public void tail(@NonNull Node node, int depth){ public void tail(@NonNull Node node, int depth){
if(node instanceof Element){ if(node instanceof Element el){
Element el=(Element)node;
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){ if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
ssb.append('…'); ssb.append("", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}else if("p".equals(el.nodeName())){ }else if("p".equals(el.nodeName())){
if(node.nextSibling()!=null) if(node.nextSibling()!=null)
ssb.append("\n\n"); ssb.append("\n\n");

View File

@@ -36,6 +36,7 @@ public class DiscoverInfoBannerHelper{
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner; case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
case TRENDING_LINKS -> R.string.trending_links_info_banner; case TRENDING_LINKS -> R.string.trending_links_info_banner;
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner; case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
}); });
} }
} }
@@ -59,6 +60,7 @@ public class DiscoverInfoBannerHelper{
TRENDING_HASHTAGS, TRENDING_HASHTAGS,
TRENDING_LINKS, TRENDING_LINKS,
LOCAL_TIMELINE, LOCAL_TIMELINE,
FEDERATED_TIMELINE,
// ACCOUNTS // ACCOUNTS
} }
} }

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
@@ -10,7 +11,6 @@ import android.content.res.TypedArray;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.InsetDrawable;
@@ -19,6 +19,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Parcelable;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
@@ -29,6 +30,7 @@ import android.webkit.MimeTypeMap;
import android.widget.Button; import android.widget.Button;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
@@ -40,8 +42,13 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked; import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus; import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID; import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
@@ -49,8 +56,10 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.SpacerSpan; import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -60,6 +69,8 @@ import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -72,6 +83,10 @@ import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@@ -87,13 +102,17 @@ public class UiUtils{
private UiUtils(){} private UiUtils(){}
public static void launchWebBrowser(Context context, String url){ public static void launchWebBrowser(Context context, String url){
if(GlobalUserPreferences.useCustomTabs){ try{
new CustomTabsIntent.Builder() if(GlobalUserPreferences.useCustomTabs){
.setShowTitle(true) new CustomTabsIntent.Builder()
.build() .setShowTitle(true)
.launchUrl(context, Uri.parse(url)); .build()
}else{ .launchUrl(context, Uri.parse(url));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); }else{
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}catch(ActivityNotFoundException x){
Toast.makeText(context, R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
} }
} }
@@ -101,7 +120,9 @@ public class UiUtils{
long t=instant.toEpochMilli(); long t=instant.toEpochMilli();
long now=System.currentTimeMillis(); long now=System.currentTimeMillis();
long diff=now-t; long diff=now-t;
if(diff<60_000L){ if(diff<1000L){
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds, diff/1000L); return context.getString(R.string.time_seconds, diff/1000L);
}else if(diff<3600_000L){ }else if(diff<3600_000L){
return context.getString(R.string.time_minutes, diff/60_000L); return context.getString(R.string.time_minutes, diff/60_000L);
@@ -330,6 +351,7 @@ public class UiUtils{
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result){
resultCallback.accept(result); resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID)); E.post(new StatusDeletedEvent(status.id, accountID));
} }
@@ -343,6 +365,88 @@ public class UiUtils{
}); });
} }
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
showConfirmationAlert(activity,
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
pinned ? R.string.pin_post : R.string.unpin_post,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
resultCallback.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (!result.pinned)
E.post(new StatusUnpinnedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
.exec(accountID);
}
);
}
public static void confirmDeleteAndRedraftPost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
showConfirmationAlert(activity, R.string.confirm_delete_and_redraft_title, R.string.confirm_delete_and_redraft, R.string.delete_and_redraft, ()->{
new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
UiUtils.redraftStatus(status, accountID, activity);
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID);
});
}
public static void redraftStatus(Status status, String accountID, Activity activity) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("hasDraft", true);
args.putString("prefilledText", HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, accountID).toString());
args.putString("spoilerText", status.spoilerText);
args.putSerializable("visibility", status.visibility);
if(status.poll!=null){
args.putInt("pollDuration", (int)status.poll.expiresAt.minus(status.createdAt.getEpochSecond(), ChronoUnit.SECONDS).getEpochSecond());
ArrayList<String> opts=status.poll.options.stream().map(o -> o.title).collect(Collectors.toCollection(ArrayList::new));
args.putStringArrayList("pollOptions", opts);
}
if(!status.mediaAttachments.isEmpty()){
ArrayList<Parcelable> serializedAttachments=status.mediaAttachments.stream()
.map(att -> Parcels.wrap(ComposeFragment.redraftAttachment(att)))
.collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("attachments", serializedAttachments);
}
Callback<Status> cb=new Callback<>(){
@Override public void onError(ErrorResponse error) {
onSuccess(null);
error.showToast(activity);
}
@Override public void onSuccess(Status status) {
if (status!=null) args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(activity, ComposeFragment.class, args);
}
};
if(status.inReplyToId!=null) new GetStatusByID(status.inReplyToId).setCallback(cb).exec(accountID);
else cb.onSuccess(null);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){ public static void setRelationshipToActionButton(Relationship relationship, Button button){
boolean secondaryStyle; boolean secondaryStyle;
if(relationship.blocking){ if(relationship.blocking){

View File

@@ -36,7 +36,7 @@ public class ImageAttachmentFrameLayout extends FrameLayout{
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return; return;
} }
int w=Math.min(((View)getParent()).getMeasuredWidth()-horizontalInset, V.dp(MAX_WIDTH)); int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH))-horizontalInset;
int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1); int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1);
int actualWidth=Math.round(tile.width/1000f*w); int actualWidth=Math.round(tile.width/1000f*w);
if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length) if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length)

View File

@@ -1,38 +1,68 @@
package org.joinmastodon.android.ui.views; package org.joinmastodon.android.ui.views;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.ui.text.ClickableLinksDelegate; import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
import org.joinmastodon.android.ui.text.DeleteWhenCopiedSpan;
public class LinkedTextView extends TextView { public class LinkedTextView extends TextView{
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this); private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
private boolean needInvalidate; private boolean needInvalidate;
private ActionMode currentActionMode;
public LinkedTextView(Context context) {
super(context); public LinkedTextView(Context context){
// TODO Auto-generated constructor stub this(context, null);
} }
public LinkedTextView(Context context, AttributeSet attrs) { public LinkedTextView(Context context, AttributeSet attrs){
super(context, attrs); this(context, attrs, 0);
// TODO Auto-generated constructor stub
} }
public LinkedTextView(Context context, AttributeSet attrs, int defStyle) { public LinkedTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle); super(context, attrs, defStyle);
// TODO Auto-generated constructor stub setCustomSelectionActionModeCallback(new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
currentActionMode=mode;
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
onTextContextMenuItem(item.getItemId());
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode){
currentActionMode=null;
}
});
} }
public boolean onTouchEvent(MotionEvent ev){ public boolean onTouchEvent(MotionEvent ev){
if(delegate.onTouch(ev)) return true; if(delegate.onTouch(ev)) return true;
return super.onTouchEvent(ev); return super.onTouchEvent(ev);
} }
public void onDraw(Canvas c){ public void onDraw(Canvas c){
super.onDraw(c); super.onDraw(c);
delegate.onDraw(c); delegate.onDraw(c);
@@ -47,4 +77,43 @@ public class LinkedTextView extends TextView {
invalidate(); invalidate();
} }
@Override
public boolean onTextContextMenuItem(int id){
if(id==android.R.id.copy){
final int selStart=getSelectionStart();
final int selEnd=getSelectionEnd();
int min=Math.max(0, Math.min(selStart, selEnd));
int max=Math.max(0, Math.max(selStart, selEnd));
final ClipData copyData=ClipData.newPlainText(null, deleteTextWithinDeleteSpans(getText().subSequence(min, max)));
ClipboardManager clipboard=getContext().getSystemService(ClipboardManager.class);
try {
clipboard.setPrimaryClip(copyData);
} catch (Throwable t) {
Log.w("LinkedTextView", t);
}
if(currentActionMode!=null){
currentActionMode.finish();
}
return true;
}
return super.onTextContextMenuItem(id);
}
private CharSequence deleteTextWithinDeleteSpans(CharSequence text){
if(text instanceof Spanned spanned){
DeleteWhenCopiedSpan[] delSpans=spanned.getSpans(0, text.length(), DeleteWhenCopiedSpan.class);
if(delSpans.length>0){
SpannableStringBuilder ssb=new SpannableStringBuilder(spanned);
for(DeleteWhenCopiedSpan span:delSpans){
int start=ssb.getSpanStart(span);
int end=ssb.getSpanStart(span);
if(start==-1)
continue;
ssb.delete(start, end+1);
}
return ssb;
}
}
return text;
}
} }

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/bookmark_selected" android:state_selected="true"/>
<item android:color="?android:textColorSecondary"/>
</selector>

View File

@@ -0,0 +1,7 @@
<vector android:height="108dp"
android:viewportHeight="48" android:viewportWidth="48"
android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group android:translateX="12" android:translateY="12">
<path android:fillColor="@color/shortcut_icon_foreground" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</group>
</vector>

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="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

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="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18zM17.502 6.25c0-0.966-0.783-1.75-1.75-1.75H8.253c-0.967 0-1.75 0.784-1.75 1.75v13.532l5.061-3.641c0.262-0.188 0.614-0.188 0.876 0l5.061 3.641V6.25z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_regular"/>
</selector>

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="M4 6.748c0-1.243 1.007-2.25 2.25-2.25h9c1.243 0 2.25 1.007 2.25 2.25V21.25c0 0.268-0.143 0.517-0.376 0.65-0.233 0.134-0.52 0.133-0.751-0.002l-5.623-3.28-5.622 3.28c-0.232 0.135-0.519 0.136-0.752 0.002C4.144 21.767 4 21.52 4 21.25V6.748zM15.25 2C17.873 2 20 4.127 20 6.75v11.873c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V6.751c0-1.796-1.455-3.25-3.25-3.25H6.637S6.75 2.942 7.434 2.42C8 2 8.602 2 8.602 2h6.648z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

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="M1 3c0-1.105 0.895-2 2-2h7c1.105 0 2 0.895 2 2v6c0 1.105-0.895 2-2 2H3c-1.105 0-2-0.895-2-2V3zm2.5 1C3.224 4 3 4.224 3 4.5S3.224 5 3.5 5h6C9.776 5 10 4.776 10 4.5S9.776 4 9.5 4h-6zm0 3C3.224 7 3 7.224 3 7.5S3.224 8 3.5 8h6C9.776 8 10 7.776 10 7.5S9.776 7 9.5 7h-6zM4 12h1.5v6.75c0 0.208 0.036 0.408 0.103 0.594l5.823-5.701c0.833-0.816 2.142-0.854 3.02-0.116l0.128 0.116 5.822 5.702c0.067-0.186 0.104-0.386 0.104-0.595V7.25c0-0.966-0.784-1.75-1.75-1.75H13V4h5.75C20.545 4 22 5.455 22 7.25v11.5c0 1.795-1.455 3.25-3.25 3.25H7.25C5.455 22 4 20.545 4 18.75V12zm15.33 8.401l-5.805-5.686c-0.265-0.26-0.675-0.283-0.966-0.071l-0.084 0.07-5.807 5.687C6.85 20.465 7.046 20.5 7.25 20.5h11.5c0.203 0 0.399-0.035 0.58-0.099zM16.253 7.5c1.244 0 2.252 1.008 2.252 2.252 0 1.244-1.008 2.252-2.252 2.252-1.244 0-2.252-1.008-2.252-2.252C14 8.508 15.008 7.5 16.252 7.5zm0 1.5C15.837 9 15.5 9.337 15.5 9.752s0.337 0.752 0.752 0.752c0.416 0 0.752-0.336 0.752-0.752C17.004 9.337 16.667 9 16.252 9z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -46,6 +46,7 @@
android:textAppearance="@style/m3_label_medium" android:textAppearance="@style/m3_label_medium"
android:textColor="?colorButtonText" android:textColor="?colorButtonText"
android:gravity="end" android:gravity="end"
android:singleLine="true"
tools:text="1:23"/> tools:text="1:23"/>
</LinearLayout> </LinearLayout>

View File

@@ -73,6 +73,28 @@
tools:text="123"/> tools:text="123"/>
</FrameLayout> </FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/bookmark"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_gravity="center"
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
android:drawablePadding="8dp"
android:drawableTint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
<Space <Space
android:layout_width="0px" android:layout_width="0px"
android:layout_height="1px" android:layout_height="1px"

View File

@@ -9,20 +9,21 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<org.joinmastodon.android.ui.views.ComposeMediaLayout <org.joinmastodon.android.ui.views.MaxWidthFrameLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"> android:layout_gravity="center"
android:maxWidth="400dp">
<ImageView <ImageView
android:id="@+id/photo" android:id="@+id/photo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:scaleType="centerCrop" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
tools:src="#0f0"/> tools:src="#0f0"/>
</org.joinmastodon.android.ui.views.ComposeMediaLayout> </org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"

View File

@@ -78,102 +78,110 @@
tools:text="Founder, CEO and lead developer @Mastodon, Germany." /> tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
<LinearLayout <LinearLayout
android:id="@+id/posts_btn" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_below="@id/bio"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123" />
<TextView
android:id="@+id/posts_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following" />
</LinearLayout>
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_toEndOf="@id/posts_btn"
android:layout_alignTop="@id/posts_btn"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/followers_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_alignTop="@id/posts_btn"
android:layout_toEndOf="@id/followers_btn"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/following_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<FrameLayout
android:id="@+id/action_btn_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_below="@id/bio"
android:layout_alignTop="@id/posts_btn" android:orientation="horizontal">
android:layout_marginTop="-8dp"
android:padding="8dp" <LinearLayout
android:layout_marginEnd="8dp" android:id="@+id/posts_btn"
android:clipToPadding="false"> android:layout_width="wrap_content"
<org.joinmastodon.android.ui.views.ProgressBarButton android:layout_height="48dp"
android:id="@+id/action_btn" android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123" />
<TextView
android:id="@+id/posts_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following" />
</LinearLayout>
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/followers_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/following_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/action_btn_wrap"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Edit Profile"/> android:layout_marginTop="8dp"
<ProgressBar android:padding="8dp"
android:id="@+id/action_progress" android:layout_marginEnd="8dp"
android:layout_width="wrap_content" android:clipToPadding="false">
android:layout_height="wrap_content" <org.joinmastodon.android.ui.views.ProgressBarButton
android:layout_gravity="center" android:id="@+id/action_btn"
android:indeterminate="true" android:layout_width="wrap_content"
style="?android:progressBarStyleSmall" android:layout_height="wrap_content"
android:elevation="10dp" android:singleLine="true"
android:outlineProvider="none" tools:text="@string/follow_back"/>
android:indeterminateTint="?colorButtonText" <ProgressBar
android:visibility="gone"/> android:id="@+id/action_progress"
</FrameLayout> android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorButtonText"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
</RelativeLayout> </RelativeLayout>

View File

@@ -3,6 +3,9 @@
<item android:id="@+id/vis_public" <item android:id="@+id/vis_public"
android:icon="@drawable/ic_fluent_earth_24_filled" android:icon="@drawable/ic_fluent_earth_24_filled"
android:title="@string/visibility_public"/> android:title="@string/visibility_public"/>
<item android:id="@+id/vis_unlisted"
android:icon="@drawable/ic_fluent_people_community_24_regular"
android:title="@string/visibility_unlisted"/>
<item android:id="@+id/vis_followers" <item android:id="@+id/vis_followers"
android:icon="@drawable/ic_fluent_people_checkmark_24_regular" android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
android:title="@string/visibility_followers_only"/> android:title="@string/visibility_followers_only"/>

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/delete" android:title="@string/delete"/> <item android:id="@+id/delete" android:title="@string/delete"/>
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
<item android:id="@+id/pin" android:title="@string/pin_post"/>
<item android:id="@+id/unpin" android:title="@string/unpin_post"/>
<item android:id="@+id/mute" android:title="@string/mute_user"/> <item android:id="@+id/mute" android:title="@string/mute_user"/>
<item android:id="@+id/block" android:title="@string/block_user"/> <item android:id="@+id/block" android:title="@string/block_user"/>
<item android:id="@+id/block_domain" android:title="@string/block_domain"/> <item android:id="@+id/block_domain" android:title="@string/block_domain"/>

View File

@@ -7,4 +7,10 @@
<item android:id="@+id/block_domain" android:title="@string/block_domain"/> <item android:id="@+id/block_domain" android:title="@string/block_domain"/>
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/> <item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/> <item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
<item
android:id="@+id/bookmarks"
android:showAsAction="always"
android:visible="false"
android:icon="@drawable/ic_fluent_bookmark_multiple_24_filled"
android:title="@string/bookmarks"/>
</menu> </menu>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<shape>
<solid android:color="@color/shortcut_icon_background"/>
<size android:width="108dp" android:height="108dp"/>
</shape>
</background>
<foreground android:drawable="@drawable/ic_compose_foreground"/>
</adaptive-icon>

View File

@@ -42,6 +42,7 @@
</plurals> </plurals>
<string name="posts">Beiträge</string> <string name="posts">Beiträge</string>
<string name="posts_and_replies">Beiträge und Antworten</string> <string name="posts_and_replies">Beiträge und Antworten</string>
<string name="pinned_posts">Angeheftet</string>
<string name="media">Medien</string> <string name="media">Medien</string>
<string name="profile_about">Über</string> <string name="profile_about">Über</string>
<string name="button_follow">Folgen</string> <string name="button_follow">Folgen</string>
@@ -121,9 +122,20 @@
<string name="action_vote">Abstimmen</string> <string name="action_vote">Abstimmen</string>
<string name="tap_to_reveal">Zum Anzeigen tippen</string> <string name="tap_to_reveal">Zum Anzeigen tippen</string>
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
<string name="delete_and_redraft">Löschen und neu erstellen</string>
<string name="confirm_delete_title">Beitrag löschen</string> <string name="confirm_delete_title">Beitrag löschen</string>
<string name="confirm_delete_and_redraft_title">Beitrag löschen und neu erstellen</string>
<string name="confirm_delete">Bist du dir sicher, dass du den Beitrag löschen möchtest?</string> <string name="confirm_delete">Bist du dir sicher, dass du den Beitrag löschen möchtest?</string>
<string name="confirm_delete_and_redraft">Bist du dir sicher, dass du den Beitrag löschen und neu erstellen möchtest?</string>
<string name="deleting">Wird gelöscht…</string> <string name="deleting">Wird gelöscht…</string>
<string name="pin_post">An Profil anheften</string>
<string name="confirm_pin_post_title">Beitrag an Profil anheften</string>
<string name="confirm_pin_post">Möchtest du den Beitrag an dein Profil anheften?</string>
<string name="pinning">Wird angeheftet…</string>
<string name="unpin_post">Von Profil lösen</string>
<string name="confirm_unpin_post_title">Angehefteten Beitrag von Profil lösen</string>
<string name="confirm_unpin_post">Bist du dir sicher, dass du den angehefteten Beitrag von deinem Profil lösen möchtest?</string>
<string name="unpinning">Wird vom Profil gelöst…</string>
<string name="notification_channel_audio_player">Audiowiedergabe</string> <string name="notification_channel_audio_player">Audiowiedergabe</string>
<string name="play">Abspielen</string> <string name="play">Abspielen</string>
<string name="pause">Pausieren</string> <string name="pause">Pausieren</string>
@@ -203,6 +215,7 @@
<string name="resent_email">Bestätigungs-E-Mail gesendet</string> <string name="resent_email">Bestätigungs-E-Mail gesendet</string>
<string name="compose_hint">Was gibt\'s Neues?</string> <string name="compose_hint">Was gibt\'s Neues?</string>
<string name="content_warning">Inhaltswarnung</string> <string name="content_warning">Inhaltswarnung</string>
<string name="image_description">Bildbeschreibung</string>
<string name="add_image_description">Füge eine Bildbeschreibung hinzu…</string> <string name="add_image_description">Füge eine Bildbeschreibung hinzu…</string>
<string name="retry_upload">Upload erneut versuchen</string> <string name="retry_upload">Upload erneut versuchen</string>
<string name="image_upload_failed">Fehler beim Hochladen des Bildes</string> <string name="image_upload_failed">Fehler beim Hochladen des Bildes</string>
@@ -213,6 +226,7 @@
<string name="alt_text_subtitle">Alternativtext erscheint für blinde Menschen. Versuche, nur so viele Details einzubeziehen, um den Kontext zu verstehen.</string> <string name="alt_text_subtitle">Alternativtext erscheint für blinde Menschen. Versuche, nur so viele Details einzubeziehen, um den Kontext zu verstehen.</string>
<string name="alt_text_hint">z.B. Eine Giraffe auf einem Dreirad während sie eine Banane isst</string> <string name="alt_text_hint">z.B. Eine Giraffe auf einem Dreirad während sie eine Banane isst</string>
<string name="visibility_public">Öffentlich</string> <string name="visibility_public">Öffentlich</string>
<string name="visibility_unlisted">Nicht gelistet</string>
<string name="visibility_followers_only">Nur Folgende</string> <string name="visibility_followers_only">Nur Folgende</string>
<string name="visibility_private">Nur Leute, die ich erwähne</string> <string name="visibility_private">Nur Leute, die ich erwähne</string>
<string name="search_all">Alle</string> <string name="search_all">Alle</string>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="shortcut_icon_background">@color/gray_700</color>
<color name="shortcut_icon_foreground">@color/primary_600</color>
</resources>

View File

@@ -92,5 +92,9 @@
<color name="highlight_over_light">#18000000</color> <color name="highlight_over_light">#18000000</color>
<color name="favorite_selected">@color/warning_500</color> <color name="favorite_selected">@color/warning_500</color>
<color name="bookmark_selected">@color/success_500</color>
<color name="boost_selected">@color/primary_500</color> <color name="boost_selected">@color/primary_500</color>
<color name="shortcut_icon_background">@color/gray_100</color>
<color name="shortcut_icon_foreground">@color/primary_700</color>
</resources> </resources>

View File

@@ -4,6 +4,7 @@
<item name="profile_posts" type="id"/> <item name="profile_posts" type="id"/>
<item name="profile_posts_with_replies" type="id"/> <item name="profile_posts_with_replies" type="id"/>
<item name="profile_pinned_posts" type="id"/>
<item name="profile_media" type="id"/> <item name="profile_media" type="id"/>
<item name="profile_about" type="id"/> <item name="profile_about" type="id"/>
@@ -12,6 +13,7 @@
<item name="discover_news" type="id"/> <item name="discover_news" type="id"/>
<item name="discover_users" type="id"/> <item name="discover_users" type="id"/>
<item name="discover_local_timeline" type="id"/> <item name="discover_local_timeline" type="id"/>
<item name="discover_federated_timeline" type="id"/>
<item name="notifications_all" type="id"/> <item name="notifications_all" type="id"/>
<item name="notifications_mentions" type="id"/> <item name="notifications_mentions" type="id"/>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Mastodon</string> <string name="app_name" translatable="false">Mastadon</string>
<string name="get_started">Get started</string> <string name="get_started">Get started</string>
<string name="log_in">Log in</string> <string name="log_in">Log in</string>
@@ -47,6 +47,7 @@
</plurals> </plurals>
<string name="posts">Posts</string> <string name="posts">Posts</string>
<string name="posts_and_replies">Posts and Replies</string> <string name="posts_and_replies">Posts and Replies</string>
<string name="pinned_posts">Pinned</string>
<string name="media">Media</string> <string name="media">Media</string>
<string name="profile_about">About</string> <string name="profile_about">About</string>
<string name="button_follow">Follow</string> <string name="button_follow">Follow</string>
@@ -126,9 +127,20 @@
<string name="action_vote">Vote</string> <string name="action_vote">Vote</string>
<string name="tap_to_reveal">Tap to reveal</string> <string name="tap_to_reveal">Tap to reveal</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="delete_and_redraft">Delete and re-draft</string>
<string name="confirm_delete_title">Delete Post</string> <string name="confirm_delete_title">Delete Post</string>
<string name="confirm_delete_and_redraft_title">Delete and re-draft Post</string>
<string name="confirm_delete">Are you sure you want to delete this post?</string> <string name="confirm_delete">Are you sure you want to delete this post?</string>
<string name="confirm_delete_and_redraft">Are you sure you want to delete and re-draft this post?</string>
<string name="deleting">Deleting…</string> <string name="deleting">Deleting…</string>
<string name="pin_post">Pin to profile</string>
<string name="confirm_pin_post_title">Pin post to profile</string>
<string name="confirm_pin_post">Do you want to pin this post to your profile?</string>
<string name="pinning">Pinning post…</string>
<string name="unpin_post">Unpin from profile</string>
<string name="confirm_unpin_post_title">Unpin post from profile</string>
<string name="confirm_unpin_post">Are you sure you want to unpin this post?</string>
<string name="unpinning">Unpinning post…</string>
<string name="notification_channel_audio_player">Audio playback</string> <string name="notification_channel_audio_player">Audio playback</string>
<string name="play">Play</string> <string name="play">Play</string>
<string name="pause">Pause</string> <string name="pause">Pause</string>
@@ -209,6 +221,7 @@
<string name="compose_hint">Type or paste what\'s on your mind</string> <string name="compose_hint">Type or paste what\'s on your mind</string>
<string name="content_warning">Content warning</string> <string name="content_warning">Content warning</string>
<string name="add_image_description">Add image description…</string> <string name="add_image_description">Add image description…</string>
<string name="image_description">Image description</string>
<string name="retry_upload">Retry upload</string> <string name="retry_upload">Retry upload</string>
<string name="image_upload_failed">Image failed to upload</string> <string name="image_upload_failed">Image failed to upload</string>
<string name="video_upload_failed">Video failed to upload</string> <string name="video_upload_failed">Video failed to upload</string>
@@ -218,6 +231,7 @@
<string name="alt_text_subtitle">Alt text describes your photos for people with low or no vision. Try to only include enough detail to understand the context.</string> <string name="alt_text_subtitle">Alt text describes your photos for people with low or no vision. Try to only include enough detail to understand the context.</string>
<string name="alt_text_hint">e.g. A dog looking around suspiciously with narrowed eyes at the camera.</string> <string name="alt_text_hint">e.g. A dog looking around suspiciously with narrowed eyes at the camera.</string>
<string name="visibility_public">Public</string> <string name="visibility_public">Public</string>
<string name="visibility_unlisted">Unlisted</string>
<string name="visibility_followers_only">Followers only</string> <string name="visibility_followers_only">Followers only</string>
<string name="visibility_private">Only people I mention</string> <string name="visibility_private">Only people I mention</string>
<string name="search_all">All</string> <string name="search_all">All</string>
@@ -278,6 +292,8 @@
<string name="button_reblog">Reblog</string> <string name="button_reblog">Reblog</string>
<string name="button_favorite">Favorite</string> <string name="button_favorite">Favorite</string>
<string name="button_share">Share</string> <string name="button_share">Share</string>
<string name="button_bookmark">Bookmark</string>
<string name="bookmarks">Bookmarks</string>
<string name="media_no_description">Media without description</string> <string name="media_no_description">Media without description</string>
<string name="add_media">Add media</string> <string name="add_media">Add media</string>
<string name="add_poll">Add a poll</string> <string name="add_poll">Add a poll</string>
@@ -307,10 +323,12 @@
<string name="downloading">Downloading…</string> <string name="downloading">Downloading…</string>
<string name="no_app_to_handle_action">There\'s no app to handle this action</string> <string name="no_app_to_handle_action">There\'s no app to handle this action</string>
<string name="local_timeline">Community</string> <string name="local_timeline">Community</string>
<string name="federated_timeline">Federation</string>
<string name="trending_posts_info_banner">These are the posts gaining traction in your corner of Mastodon.</string> <string name="trending_posts_info_banner">These are the posts gaining traction in your corner of Mastodon.</string>
<string name="trending_hashtags_info_banner">These are the hashtags gaining traction in your corner of Mastodon.</string> <string name="trending_hashtags_info_banner">These are the hashtags gaining traction in your corner of Mastodon.</string>
<string name="trending_links_info_banner">These are the news stories being shared the most in your corner of Mastodon.</string> <string name="trending_links_info_banner">These are the news stories being shared the most in your corner of Mastodon.</string>
<string name="local_timeline_info_banner">These are the most recent posts by the people who use the same Mastodon server as you.</string> <string name="local_timeline_info_banner">These are the most recent posts by the people who use the same Mastodon server as you.</string>
<string name="federated_timeline_info_banner">These are the most recent posts by the people in your federation.</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="see_new_posts">See new posts</string> <string name="see_new_posts">See new posts</string>
<string name="load_missing_posts">Load missing posts</string> <string name="load_missing_posts">Load missing posts</string>
@@ -339,4 +357,5 @@
<item quantity="other">%,d reblogs</item> <item quantity="other">%,d reblogs</item>
</plurals> </plurals>
<string name="timestamp_via_app">%1$s via %2$s</string> <string name="timestamp_via_app">%1$s via %2$s</string>
<string name="time_now">now</string>
</resources> </resources>