Compare commits

..

30 Commits

Author SHA1 Message Date
sk
a354ea80ab Merge remote-tracking branch 'upstream/l10n_master' 2023-06-06 17:09:06 +02:00
sk22
9f65b8112a Translated using Weblate (German)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-06 15:08:53 +00:00
Choukajohn
6ac5d957fe Translated using Weblate (French)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-06 15:05:05 +00:00
sk
4258c55b88 implement fetching listings from remote instances 2023-06-06 17:04:29 +02:00
sk
969f29e2e9 support string res for small text item 2023-06-06 16:55:20 +02:00
sk
68921d0f0b fix footer being too close to next header 2023-06-06 14:23:34 +02:00
Eugen Rochko
c4ac4ee173 New translations strings.xml (Swedish) 2023-06-06 13:23:46 +02:00
Eugen Rochko
659b4e2fcd New translations strings.xml (Swedish) 2023-06-06 11:55:59 +02:00
sk
24e5bda8d3 fix profile options icons 2023-06-06 10:30:32 +02:00
Eugen Rochko
02b1ad8d7a New translations strings.xml (Swedish) 2023-06-06 00:56:52 +02:00
Eugen Rochko
47eeb01b75 New translations strings.xml (Turkish) 2023-06-05 21:08:28 +02:00
ihor_ck
4288814138 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (293 of 293 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-05 16:09:08 +00:00
sk
ac4458e106 fix badged toolbar icons with new appkit version 2023-06-05 18:07:22 +02:00
sk
3d24b2de10 add status counters event listener to notifications 2023-06-05 16:19:55 +02:00
sk
ed994b23e9 fix wrong views' states being modified
closes sk22#549
2023-06-05 16:12:07 +02:00
sk
8c4678aba5 fix updated main status not being applied 2023-06-05 16:05:58 +02:00
Eugen Rochko
3d5fb2dfea New translations strings.xml (Swedish) 2023-06-05 15:00:47 +02:00
Eugen Rochko
ef6238b593 New translations strings.xml (Swedish) 2023-06-05 13:45:02 +02:00
Eugen Rochko
bc9bec3d66 New translations strings.xml (Swedish) 2023-06-05 12:47:38 +02:00
sk
d16e199dd1 scroll up when posting on profile fragment
closes sk22#546
2023-06-05 11:48:40 +02:00
sk
a9c2df2e83 do copy spoilerRevealed on clone
closes sk22#547
2023-06-05 11:26:36 +02:00
sk
4673a4b9f7 add missing database column in post notification table 2023-06-05 11:22:01 +02:00
Grishka
d4a5286895 Fix #553 2023-06-04 23:47:08 +02:00
Grishka
1b4579346b Fix #548 2023-06-04 23:39:06 +02:00
sk
0665b8dd3b fix incompatibility with upstream bugfix 2023-06-04 23:33:04 +02:00
Grishka
853124e2ce Fix it again 2023-06-04 23:32:13 +02:00
Grishka
5dcd6e5a0d Fix it again 2023-06-04 23:31:12 +02:00
Grishka
6f25c8be0f Fix #583 2023-06-04 23:26:48 +02:00
sk
1db4b1319e use latest appkit version 2023-06-04 23:26:39 +02:00
Grishka
76a97fcb47 Fix #591 2023-06-04 23:20:29 +02:00
43 changed files with 659 additions and 208 deletions

View File

@@ -75,7 +75,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'me.grishka.appkit:appkit:1.2.8'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -53,24 +53,30 @@ public class ThreadFragmentTest {
}
@Test
public void updateMainStatus() {
public void maybeApplyMainStatus() {
ThreadFragment fragment = new ThreadFragment();
fragment.contextInitiallyRendered = true;
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update1.editedAt = Instant.ofEpochSecond(1);
fragment.updatedStatus = update1;
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.updateMainStatus();
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("fired update event", update1, event1.status);
assertEquals("updated main status", update1, fragment.mainStatus);
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
update2.favouritesCount = 123;
fragment.updatedStatus = update2;
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.updateMainStatus();
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
assertEquals("only fired counter update event", update2.id, event2.id);
assertEquals("updated counter is correct", 123, event2.favorites);
assertEquals("updated main status", update2, fragment.mainStatus);
Status update3 = Status.ofFake("123456", "whatever", Instant.EPOCH);
fragment.contextInitiallyRendered = false;
fragment.updatedStatus = update3;
assertNull("no update when context hasn't been rendered", fragment.maybeApplyMainStatus());
}
@Test

View File

@@ -48,6 +48,7 @@ public class GlobalUserPreferences{
public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
@@ -127,6 +128,7 @@ public class GlobalUserPreferences{
replyVisibility=prefs.getString("replyVisibility", null);
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -176,6 +178,7 @@ public class GlobalUserPreferences{
.putString("replyVisibility", replyVisibility)
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.apply();
}

View File

@@ -36,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final int DB_VERSION=4;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
@@ -61,7 +61,7 @@ public class CacheController{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
@@ -112,7 +112,7 @@ public class CacheController{
runOnDbThread((db)->{
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
@@ -120,6 +120,7 @@ public class CacheController{
if(s.hasGapAfter)
flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags);
values.put("time", s.createdAt.getEpochSecond());
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -134,7 +135,7 @@ public class CacheController{
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
@@ -192,7 +193,7 @@ public class CacheController{
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Notification n:notifications){
if(n.type==null){
continue;
@@ -200,6 +201,7 @@ public class CacheController{
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
values.put("time", n.createdAt.getEpochSecond());
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -296,21 +298,24 @@ public class CacheController{
CREATE TABLE `home_timeline` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0
`flags` INTEGER NOT NULL DEFAULT 0,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_all` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_mentions` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
createRecentSearchesTable(db);
createPostsNotificationsTable(db);
@@ -318,12 +323,16 @@ public class CacheController{
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion==1){
if(oldVersion<2){
createRecentSearchesTable(db);
}
if(oldVersion==2){
if(oldVersion<3){
// MEGALODON-SPECIFIC
createPostsNotificationsTable(db);
}
if(oldVersion<4){
addTimeColumns(db);
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -341,9 +350,21 @@ public class CacheController{
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
}
private void addTimeColumns(SQLiteDatabase db){
db.execSQL("DELETE FROM `home_timeline`");
db.execSQL("DELETE FROM `notifications_all`");
db.execSQL("DELETE FROM `notifications_mentions`");
db.execSQL("DELETE FROM `notifications_posts`");
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface

View File

@@ -20,9 +20,11 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
@@ -44,7 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
TypeToken<T> respTypeToken;
Call okhttpCall;
Token token;
boolean canceled;
boolean canceled, isRemote;
Map<String, String> headers;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@@ -101,6 +103,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> execRemote(String domain) {
return execRemote(domain, null);
}
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
this.isRemote = true;
return Optional.ofNullable(remoteSession)
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
.filter(acc -> acc.domain.equals(domain))
.findAny())
.map(AccountSession::getID)
.map(this::exec)
.orElse(this.execNoAuth(domain));
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
}
@@ -167,6 +184,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
@CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){
((BaseModel) respObj).isRemote = isRemote;
((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){
if(removeUnsupportedItems){
@@ -175,6 +193,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Object item=itr.next();
if(item instanceof BaseModel){
try{
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}catch(ObjectValidationException x){
Log.w(TAG, "Removing invalid object from list", x);
@@ -182,15 +201,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
}
}
// no idea why we're post-processing twice, but well, as long
// as upstream does it like this, i don't wanna break anything
for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel){
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}else{
for(Object item:((List<?>) respObj)){
if(item instanceof BaseModel)
if(item instanceof BaseModel) {
((BaseModel) item).isRemote = isRemote;
((BaseModel) item).postprocess();
}
}
}
}

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
/**
* note that this method usually only returns a result if the instance already knows about an
* account - so it makes sense for looking up local users, search might be preferred otherwise
*/
public GetAccountByHandle(String acct){
super(HttpMethod.GET, "/accounts/lookup", Account.class);
addQueryParameter("acct", acct);
}
}

View File

@@ -160,6 +160,11 @@ public class AccountSessionManager{
return sessions.get(id);
}
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
}
@Nullable
public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null)

View File

@@ -20,6 +20,7 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
@@ -95,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){
if(ev.status.mediaAttachments.isEmpty())
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
return;
}
prependItems(Collections.singletonList(ev.status), true);
if (isOnTop()) scrollToTop();
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){

View File

@@ -71,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -680,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
protected int getListWidthForMediaLayout(){
return list.getWidth();
}

View File

@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
toolbar.setNavigationContentDescription(R.string.back);
}
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false; // else, badged icons don't work :(
}
}

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
@@ -25,6 +26,8 @@ import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
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.TextStatusDisplayItem;
@@ -227,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
// copied from StatusListFragment.EventListener (just like the method above)
// (which assumes this.data to be a list of statuses...)
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Notification n:data){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
footer.rebind();
}
}
}
}
for(Notification n:preloadedData){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)

View File

@@ -45,6 +45,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -58,7 +59,6 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -138,7 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private TextView followsYouView;
private ViewGroup rolesView;
private Account account;
private Account account, remoteAccount;
private String accountID;
private String domain;
private Relationship relationship;
@@ -177,7 +177,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
if (getArguments().containsKey("remoteAccount")) {
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
if(!getArguments().getBoolean("noAutoLoad", false))
loadData();
} else if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
@@ -348,36 +352,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper;
}
private void onAccountLoaded(Account result) {
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@Override
protected void doLoadData(){
if (remoteAccount != null) {
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
if (getContext() == null) return;
if (args == null || !args.containsKey("profileAccount")) {
onError(new MastodonErrorResponse(
getContext().getString(R.string.sk_error_loading_profile),
0, null
));
return;
}
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
});
return;
}
currentRequest=new GetAccountByID(profileAccountID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(Account result){
if (getActivity() == null) return;
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
bindHeaderView();
dataLoaded();
if(!tabLayoutMediator.isAttached())
tabLayoutMediator.attach();
if(!isOwnProfile)
loadRelationship();
else
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
onAccountLoaded(result);
}
})
.exec(accountID);
@@ -491,9 +514,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
if (account.roles != null && !account.roles.isEmpty()) {
rolesView.setVisibility(View.VISIBLE);
@@ -512,13 +535,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(domain);
}
ssb.append(acct);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
@@ -527,7 +549,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
username.setText('@'+acct);
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@@ -597,6 +619,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return false;
}
@Override
protected boolean wantsToolbarMenuIconsTinted() {
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){

View File

@@ -219,6 +219,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.confirmBeforeReblog=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
GlobalUserPreferences.allowRemoteLoading=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
items.add(new HeaderItem(R.string.sk_timelines));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -835,7 +840,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
}
private class SmallTextItem extends Item {
private String text;
private final String text;
public SmallTextItem(@StringRes int text) {
this.text = getString(text);
}
public SmallTextItem(String text) {
this.text = text;

View File

@@ -68,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
status.filterRevealed = true;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);

View File

@@ -44,11 +44,12 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private boolean initialAnimationFinished;
protected boolean contextInitiallyRendered;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -110,7 +111,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override
protected void doLoadData(int offset, int count){
refreshMainStatus();
loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
@@ -154,21 +155,28 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
adapter.notifyDataSetChanged();
}
list.scrollToPosition(displayItems.size()-count);
// no animation is going to happen, so proceeding to apply right now
if (data.size() == 1) {
contextInitiallyRendered = true;
// for the case that the main status has already finished loading
maybeApplyMainStatus();
}
}
})
.exec(accountID);
}
private void refreshMainStatus() {
private void loadMainStatus() {
new GetStatusByID(mainStatus.id)
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status status) {
if (getContext() == null || status == null) return;
updatedStatus = status;
// only update main status if the initial animation is already finished.
// otherwise, the animator will call it in onAnimationFinished
if (initialAnimationFinished || data.size() == 1) updateMainStatus();
// for the case that the context has already loaded (and the animation has
// already finished), falling back to applying it ourselves:
maybeApplyMainStatus();
}
@Override
@@ -176,7 +184,9 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}).exec(accountID);
}
protected Object updateMainStatus() {
protected Object maybeApplyMainStatus() {
if (updatedStatus == null || !contextInitiallyRendered) return null;
// returning fired event object to facilitate testing
Object event;
if (updatedStatus.editedAt != null &&
@@ -287,15 +297,14 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
showContent();
if(!loaded)
footerProgress.setVisibility(View.VISIBLE);
list.setItemAnimator(new BetterItemAnimator() {
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);
// in case someone else is about to call updateMainStatus faster...
initialAnimationFinished = true;
// ...if not (someone did fetch it but the animation wasn't finished yet),
// call it now
if (updatedStatus != null) updateMainStatus();
contextInitiallyRendered = true;
// for the case that both requests are already done (and thus won't apply it)
maybeApplyMainStatus();
}
});
}
@@ -357,4 +366,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
}
}
@Override
protected void onErrorRetryClick(){
if(preloadingFailed){
preloadingFailed=false;
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
V.setVisibilityAnimated(footerError, View.GONE);
doLoadData();
return;
}
super.onErrorRetryClick();
}
}

View File

@@ -3,16 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
protected Account account;
protected String initialSubtitle = "";
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
if (getArguments().containsKey("remoteAccount")) {
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
}
setTitle("@"+account.acct);
}
@@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
? "/users/" + account.id
: '@' + account.acct).build();
}
@Override
public String getRemoteDomain() {
return account.getDomainFromURL();
}
@Override
public Account getCurrentInfo() {
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
}
@Override
protected MastodonAPIRequest<Account> loadRemoteInfo() {
return new GetAccountByHandle(account.acct);
}
@Override
protected AccountSession getRemoteSession() {
return Optional.ofNullable(remoteInfo)
.map(AccountSessionManager.getInstance()::tryGetAccount)
.orElse(null);
}
@Override
protected void onRemoteLoadingFailed() {
super.onRemoteLoadingFailed();
String prefix = initialSubtitle == null ? "" :
initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " ";
String str = prefix +
getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain);
setSubtitle(str);
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
@@ -47,6 +48,7 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -243,10 +245,13 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship();
}
@@ -282,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@@ -423,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
}
}

View File

@@ -13,12 +13,12 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count);
return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -13,12 +13,12 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowing(account.id, maxID, count);
return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -1,33 +1,173 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
private String nextMaxID;
private MastodonAPIRequest<T> remoteInfoRequest;
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
protected int localOffset;
protected T remoteInfo;
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
protected abstract MastodonAPIRequest<T> loadRemoteInfo();
public abstract T getCurrentInfo();
public abstract String getRemoteDomain();
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// already have remote info (e.g. from arguments), so no need to fetch it again
if (remoteInfo != null) {
onRemoteInfoLoaded(remoteInfo);
return;
}
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|| getSession().domain.equals(getRemoteDomain());
if (!remoteDisabled) {
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
@Override
public void onSuccess(T result) {
if (getContext() == null) return;
onRemoteInfoLoaded(result);
}
@Override
public void onError(ErrorResponse error) {
if (getContext() == null) return;
onRemoteLoadingFailed();
}
});
remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession());
}
}
/**
* override to provide an ideal account session (e.g. if you're logged into the author's remote
* account) to make the remote request from. if null is provided, will try to get any session
* on the remote domain, or tries the request without authentication.
*/
protected AccountSession getRemoteSession() {
return null;
}
protected void onRemoteInfoLoaded(T info) {
this.remoteInfo = info;
this.remoteInfoRequest = null;
maybeStartLoadingRemote();
}
protected void onRemoteLoadingFailed() {
this.remoteRequestFailed = true;
this.remoteInfo = null;
this.remoteInfoRequest = null;
if (doneWithHomeInstance) dataLoaded();
}
@Override
public void dataLoaded() {
super.dataLoaded();
footerProgress.setVisibility(View.GONE);
}
private void maybeStartLoadingRemote() {
if (startedRemoteLoading || remoteDisabled) return;
if (!remoteRequestFailed) {
if (data.size() == 0) showProgress();
else footerProgress.setVisibility(View.VISIBLE);
}
if (doneWithHomeInstance && remoteInfo != null) {
startedRemoteLoading = true;
loadData(localOffset, itemsPerPage * 2);
}
}
@Override
public void onRefresh() {
localOffset = 0;
doneWithHomeInstance = false;
startedRemoteLoading = false;
super.onRefresh();
}
@Override
public void loadData(int offset, int count) {
// always subtract the amount loaded through the home instance once loading from remote
// since loadData gets called with data.size() (data includes both local and remote)
if (doneWithHomeInstance) offset -= localOffset;
super.loadData(offset, count);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
MastodonAPIRequest<?> request = onCreateRequest(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
List<AccountItem> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url)))
.map(AccountItem::new)
.collect(Collectors.toList());
boolean hasMore = nextMaxID != null;
if (!hasMore && !doneWithHomeInstance) {
// only runs last time data was fetched from the home instance
localOffset = d.size() + items.size();
doneWithHomeInstance = true;
}
onDataLoaded(items, hasMore);
if (doneWithHomeInstance) maybeStartLoadingRemote();
}
})
.exec(accountID);
@Override
public void onError(ErrorResponse error) {
if (doneWithHomeInstance) {
onRemoteLoadingFailed();
onDataLoaded(Collections.emptyList(), false);
return;
}
super.onError(error);
}
});
if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting
if (doneWithHomeInstance && remoteInfo != null) {
request.execRemote(getRemoteDomain(), getRemoteSession());
} else {
request.exec(accountID);
}
currentRequest = request;
}
@Override

View File

@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusFavorites(status.id, maxID, count);
return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
updateTitle(status);
}
@Override
protected void updateTitle(Status status) {
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetStatusReblogs(status.id, maxID, count);
return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
}
@Override

View File

@@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
import java.util.Optional;
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
protected Status status;
protected abstract void updateTitle(Status status);
protected MastodonAPIRequest<Status> loadRemoteInfo() {
String[] parts = status.url.split("/");
if (parts.length == 0) return null;
return new GetStatusByID(parts[parts.length - 1]);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
@Override
protected boolean hasSubtitle(){
return false;
return remoteRequestFailed;
}
@Override
@@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
: '@' + status.account.acct + '/' + status.id)
.build();
}
@Override
public String getRemoteDomain() {
return Uri.parse(status.url).getHost();
}
@Override
public Status getCurrentInfo() {
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : status;
}
@Override
protected AccountSession getRemoteSession() {
return Optional.ofNullable(remoteInfo)
.map(s -> s.account)
.map(AccountSessionManager.getInstance()::tryGetAccount)
.orElse(null);
}
@Override
protected void onRemoteInfoLoaded(Status info) {
super.onRemoteInfoLoaded(info);
updateTitle(remoteInfo);
}
@Override
protected void onRemoteLoadingFailed() {
super.onRemoteLoadingFailed();
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain));
updateToolbar();
}
}

View File

@@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -17,7 +16,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
public class DiscoverPostsFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
@Override
@@ -39,11 +38,6 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;

View File

@@ -12,7 +12,6 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
@@ -43,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
@@ -312,11 +311,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
}
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search");

View File

@@ -1,7 +1,10 @@
package org.joinmastodon.android.model;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@@ -135,6 +138,8 @@ public class Account extends BaseModel implements Searchable{
public List<Role> roles;
public @Nullable String fqn; // akkoma has this, mastodon't
@Override
public String getQuery() {
return url;
@@ -162,6 +167,7 @@ public class Account extends BaseModel implements Searchable{
moved.postprocess();
if(TextUtils.isEmpty(displayName))
displayName=username;
if(fqn == null) fqn = getFullyQualifiedName();
}
public boolean isLocal(){
@@ -173,6 +179,10 @@ public class Account extends BaseModel implements Searchable{
return parts.length==1 ? null : parts[1];
}
public String getDomainFromURL() {
return Uri.parse(url).getHost();
}
public String getDisplayUsername(){
return '@'+acct;
}
@@ -181,6 +191,10 @@ public class Account extends BaseModel implements Searchable{
return '@'+acct.split("@")[0];
}
public String getFullyQualifiedName() {
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
}
@Override
public String toString(){
return "Account{"+

View File

@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
public abstract class BaseModel implements Cloneable{
/**
* indicates the profile has been fetched from a foreign instance.
*
* @see MastodonAPIRequest#execRemote
*/
public transient boolean isRemote;
public abstract class BaseModel{
@CallSuper
public void postprocess() throws ObjectValidationException{
try{
@@ -23,4 +32,14 @@ public abstract class BaseModel{
}
}catch(IllegalAccessException ignore){}
}
@NonNull
@Override
public Object clone(){
try{
return super.clone();
}catch(CloneNotSupportedException x){
throw new RuntimeException(x);
}
}
}

View File

@@ -30,10 +30,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
@NonNull
@Override
public PushSubscription clone(){
PushSubscription copy=null;
try{
copy=(PushSubscription) super.clone();
}catch(CloneNotSupportedException ignore){}
PushSubscription copy=(PushSubscription) super.clone();
copy.alerts=alerts.clone();
return copy;
}

View File

@@ -20,6 +20,8 @@ import java.lang.reflect.Type;
import java.time.Instant;
import java.util.List;
import androidx.annotation.NonNull;
@Parcel
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
@RequiredField
@@ -191,7 +193,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
}
public static class StatusDeserializer implements JsonDeserializer<Status> {
@Override
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();

View File

@@ -5,7 +5,6 @@ import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -125,9 +123,9 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(FooterStatusDisplayItem item){
bindButton(replies, item.status.repliesCount);
bindButton(boosts, item.status.reblogsCount);
bindButton(favorites, item.status.favouritesCount);
bindText(replies, item.status.repliesCount);
bindText(boosts, item.status.reblogsCount);
bindText(favorites, item.status.favouritesCount);
// in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
@@ -146,12 +144,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
condenseBottom ? V.dp(-5) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){
private void bindText(TextView btn, long count){
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(8));
@@ -215,13 +213,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
onBoostLongClick(v);
return;
}
boosts.setSelected(!item.status.reblogged);
boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
}
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boosts, r.reblogsCount);
bindText(boosts, r.reblogsCount);
}
private boolean onBoostLongClick(View v){
@@ -307,10 +305,10 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onFavoriteClick(View v){
favorites.setSelected(!item.status.favourited);
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
bindButton(favorites, r.favouritesCount);
bindText(favorites, r.favouritesCount);
});
}

View File

@@ -241,7 +241,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
int bottomPadding = (translateVisible && nextIsFooter) ? 0
: nextIsFooter ? V.dp(8)
: nextIsFooter ? V.dp(6)
: V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);

View File

@@ -4,8 +4,7 @@ import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.view.GestureDetector;
@@ -33,7 +32,8 @@ public class ClickableLinksDelegate {
hlPaint=new Paint();
hlPaint.setAntiAlias(true);
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
hlPaint.setStrokeWidth(V.dp(4));
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
}
@@ -58,7 +58,7 @@ public class ClickableLinksDelegate {
public void onDraw(Canvas canvas){
if(hlPath!=null){
canvas.save();
canvas.translate(0, view.getPaddingTop());
canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
canvas.drawPath(hlPath, hlPaint);
canvas.restore();
}
@@ -73,59 +73,29 @@ public class ClickableLinksDelegate {
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent event) {
int line=-1;
Rect rect=new Rect();
Layout l=view.getLayout();
for(int i=0;i<l.getLineCount();i++){
view.getLineBounds(i, rect);
if(rect.contains((int)event.getX(), (int)event.getY())){
line=i;
break;
}
}
if(line==-1){
int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
float x=event.getX(), y=event.getY();
if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
return false;
}
x-=padLeft;
y-=padTop;
Layout l=view.getLayout();
int line=l.getLineForVertical(Math.round(y));
int position=l.getOffsetForHorizontal(line, x);
CharSequence text=view.getText();
if(text instanceof Spanned s){
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
int lstart=l.getLineForOffset(start);
int lend=l.getLineForOffset(end);
if(line>=lstart && line<=lend){
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
continue;
}
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
continue;
}
hlPath=new Path();
selectedSpan=span;
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
Rect bounds=new Rect();
l.getLineBounds(j, bounds);
//bounds.left+=view.getPaddingLeft();
if(j==lstart){
bounds.left=Math.round(l.getPrimaryHorizontal(start));
}
if(j==lend){
bounds.right=Math.round(l.getPrimaryHorizontal(end));
}else{
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
}
bounds.inset(V.dp(-2), V.dp(-2));
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
}
hlPath.offset(view.getPaddingLeft(), 0);
view.invalidate();
return true;
}
for(LinkSpan span:spans){
int start=s.getSpanStart(span);
int end=s.getSpanEnd(span);
if(start<=position && end>position){
selectedSpan=span;
hlPath=new Path();
l.getSelectionPath(start, end, hlPath);
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
view.invalidate();
return true;
}
}
}

View File

@@ -56,6 +56,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
@@ -1136,6 +1137,12 @@ public class UiUtils {
}
}
public static void lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
parseFediverseHandle(query).ifPresentOrElse(
handle -> lookupAccountHandle(context, accountID, handle, go),
() -> go.accept(null, null)
);
}
public static void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
@@ -1153,12 +1160,18 @@ public class UiUtils {
return;
}
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
args.putString("error", context.getString(R.string.sk_resource_not_found));
go.accept(null, null);
}
@Override
public void onError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
go.accept(null, args);
}
}).exec(accountID);
}
@@ -1395,7 +1408,7 @@ public class UiUtils {
extras.putString("account", accountID);
if (n.status!=null) {
Status status=n.status;
extras.putParcelable("status", Parcels.wrap(status));
extras.putParcelable("status", Parcels.wrap(status.clone()));
Nav.go((Activity) context, ThreadFragment.class, extras);
} else if (n.report != null) {
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_megaphone_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

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="M12 4.5c-4.694 0-8.5 3.806-8.5 8.5 0 2.345 0.948 4.466 2.484 6.005 0.293 0.293 0.293 0.768 0 1.06-0.294 0.293-0.769 0.293-1.061 0C3.118 18.256 2 15.758 2 13 2 7.477 6.477 3 12 3s10 4.477 10 10c0 2.758-1.118 5.256-2.923 7.065-0.292 0.293-0.767 0.293-1.06 0-0.293-0.292-0.294-0.767-0.001-1.06C19.552 17.467 20.5 15.345 20.5 13c0-4.694-3.806-8.5-8.5-8.5zM12 8c-2.761 0-5 2.239-5 5 0 1.382 0.56 2.632 1.466 3.537 0.293 0.293 0.293 0.768 0 1.06-0.292 0.294-0.767 0.294-1.06 0.001C6.229 16.423 5.5 14.796 5.5 13c0-3.59 2.91-6.5 6.5-6.5s6.5 2.91 6.5 6.5c0 1.796-0.73 3.423-1.906 4.598-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06C16.44 15.631 17 14.381 17 13c0-2.761-2.239-5-5-5zm0 2.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5-1.12-2.5-2.5-2.5zM11 13c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_person_add_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

View File

@@ -3,7 +3,7 @@
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>

View File

@@ -292,4 +292,8 @@
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</string>
<string name="sk_open_in_app">In App öffnen</string>
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
<string name="sk_error_loading_profile">Konnte das Profil auf deiner Heim-Instanz nicht laden.</string>
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower*innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
</resources>

View File

@@ -290,7 +290,8 @@
<string name="sk_open_in_app">Ouvrir dans l\'application</string>
<string name="sk_external_share_title">Partager avec le compte</string>
<string name="sk_external_share_or_open_title">Partager ou ouvrir avec le compte</string>
<string name="sk_bubble_timeline_info_banner">Ce sont les publications les plus récentes des personnes présentes dans la bulle de votre serveur Akkoma.</string>
<string name="sk_bubble_timeline_info_banner">Ce sont les messages les plus récents du réseau organisés par vos administrateurs d\'instance.</string>
<string name="sk_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
<string name="sk_open_in_app_failed">Impossible de l\'ouvrir dans l\'application</string>
</resources>

View File

@@ -10,11 +10,13 @@
<string name="ok">OK</string>
<string name="preparing_auth">Förbereder för autentisering…</string>
<string name="finishing_auth">Slutför autentisering…</string>
<string name="user_boosted">%s boostade</string>
<string name="in_reply_to">Som svar på %s</string>
<string name="notifications">Notiser</string>
<string name="user_followed_you">följde dig</string>
<string name="user_sent_follow_request">skickade en förfrågning om att följa till dig</string>
<string name="user_favorited">favoritmarkerade dit inlägg</string>
<string name="notification_boosted">boostade ditt inlägg</string>
<string name="poll_ended">omröstning avslutad</string>
<string name="time_seconds">%ds</string>
<string name="time_minutes">%dm</string>
@@ -164,6 +166,7 @@
<string name="report_sent_subtitle">Medan vi granskar detta kan du vidta åtgärder mot %s.</string>
<string name="unfollow_user">Avfölj %s</string>
<string name="unfollow">Avfölj</string>
<string name="mute_user_explain">Du kommer inte att se deras inlägg eller boosts i ditt hemflöde. De kommer inte veta att de har blivit tystade.</string>
<string name="block_user_explain">De kommer inte längre att kunna följa eller se dina inlägg, men de kan se om de har blockerats.</string>
<string name="report_personal_title">Vill du inte se det här?</string>
<string name="report_personal_subtitle">När du ser något som du inte gillar på Mastodon kan du ta bort personen från din upplevelse.</string>
@@ -412,7 +415,18 @@
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s tillåter inte registrering från %2$s. Prova en annan eller &lt;a&gt;välj en annan server&lt;/a&gt;.</string>
<string name="signup_username_taken">Det här användarnamnet är redan taget.</string>
<string name="spoiler_show">Visa ändå</string>
<string name="poll_multiple_choice">Välj en eller flera</string>
<string name="save_changes">Spara ändringar</string>
<string name="profile_timeline">Tidslinje</string>
<string name="view_all">Visa alla</string>
<string name="profile_endorsed_accounts">Konton</string>
<string name="verified_link">Verifierad länk</string>
<string name="show">Visa</string>
<string name="hide">Dölj</string>
<string name="join_default_server">Gå med %s</string>
<string name="signup_or_login">eller</string>
<string name="learn_more">Läs mer</string>
<string name="welcome_to_mastodon">Välkommen till Mastodon</string>
<string name="what_are_servers">Vad är servrar?</string>
</resources>

View File

@@ -6,17 +6,17 @@
<string name="next">Sonraki</string>
<string name="loading_instance">Sunucu bilgisi alınıyor…</string>
<string name="error">Hata</string>
<string name="not_a_mastodon_instance">%s bir Mastodon sunucusu gibi görünmüyor.</string>
<string name="not_a_mastodon_instance">%s bir Mastodon sunucusu gibi görükmüyor.</string>
<string name="ok">Tamam</string>
<string name="preparing_auth">Kimlik doğrulama için hazırlanıyor…</string>
<string name="finishing_auth">Kimlik doğrulama tamamlanıyor…</string>
<string name="user_boosted">%s yineledi</string>
<string name="user_boosted">%s paylaştı</string>
<string name="in_reply_to">%s için yanıt</string>
<string name="notifications">Bildirimler</string>
<string name="user_followed_you">sizi takip etti</string>
<string name="user_sent_follow_request">sana bir takip isteği gönderdi</string>
<string name="user_favorited">gönderinizi favorilerine ekledi</string>
<string name="notification_boosted">gönderinizi yineledi</string>
<string name="notification_boosted">gönderinizi paylaştı</string>
<string name="poll_ended">oylama sona erdi</string>
<string name="time_seconds">%ds</string>
<string name="time_minutes">%ddk</string>
@@ -123,7 +123,7 @@
<string name="delete">Sil</string>
<string name="confirm_delete_title">Gönderiyi sil</string>
<string name="confirm_delete">Bu gönderiyi silmek istediğinizden emin misiniz?</string>
<string name="deleting">Siliniyor...</string>
<string name="deleting">Siliniyor</string>
<string name="notification_channel_audio_player">Ses çal</string>
<string name="play">Oynat</string>
<string name="pause">Durdur</string>
@@ -149,7 +149,7 @@
<string name="report_choose_reason_subtitle">En iyi eşleşmeyi seçin</string>
<string name="report_reason_personal">Hoşuma gitmiyor</string>
<string name="report_reason_personal_subtitle">Görmek isteyeceğin bir şey değil</string>
<string name="report_reason_spam">Bu spam</string>
<string name="report_reason_spam">Spam</string>
<string name="report_reason_spam_subtitle">Kötü amaçlı bağlantılar, sahte etkileşim veya tekrarlayan yanıtlar</string>
<string name="report_reason_violation">Sunucu kurallarını ihlal ediyor</string>
<string name="report_reason_violation_subtitle">Belirli kuralları çiğnediğinin farkındasınız</string>
@@ -166,7 +166,7 @@
<string name="report_sent_subtitle">Biz bunu incelerken siz %s karşı önlem alabilirsiniz.</string>
<string name="unfollow_user">Takipten çık %s</string>
<string name="unfollow">Takipten çık</string>
<string name="mute_user_explain">Ana sayfa akışınızda kişinin gönderilerini görmeyeceksiniz. Sessize alındıklarını bilemeyecekler.</string>
<string name="mute_user_explain">Anasayfa akışınızda kişinin gönderilerini görmeyeceksiniz. Sessize alındıklarını bilemeyecekler.</string>
<string name="block_user_explain">Artık sizi takip edemez ve gönderilerinizi göremezler ama engellendiklerini görebilirler.</string>
<string name="report_personal_title">Bunu görmek istemiyor musun?</string>
<string name="report_personal_subtitle">Mastodon\'da beğenmediğiniz bir şey gördüğünüzde, o kişiyi deneyiminizden çıkarabilirsiniz.</string>
@@ -200,10 +200,10 @@
<string name="confirm_email_title">E-posta Kutunuzu Kontrol Edin</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">%s işleminizi doğrulamak için size gönderdiğimiz linke tıklayınız. Biz burada bekleyeceğiz.</string>
<string name="confirm_email_didnt_get">Link size ulaşmadı mı?</string>
<string name="confirm_email_didnt_get">Bağlantı size ulaşmadı mı?</string>
<string name="resend">Yeniden gönder</string>
<string name="open_email_app">E-posta uygulamasını</string>
<string name="resent_email">Onay e-postası gönderildi</string>
<string name="open_email_app">Eposta uygulamasını</string>
<string name="resent_email">Onay epostası gönderildi</string>
<string name="compose_hint">Aklınızdan geçenleri yazın veya yapıştırın</string>
<string name="content_warning">İçerik Uyarısı</string>
<string name="add_image_description">Resim açıklaması ekle…</string>
@@ -243,18 +243,18 @@
<string name="settings_gif">Animasyonlu avatarları ve emojileri oynat</string>
<string name="settings_custom_tabs">Uygulama içi tarayıcıyı kullan</string>
<string name="settings_notifications">Bildirimler</string>
<string name="notify_me_when">Beni şu durumda bilgilendir: </string>
<string name="notify_anyone">Herhangi biri</string>
<string name="notify_me_when">Beni şu durumda bilgilendir</string>
<string name="notify_anyone">Herhangibiri</string>
<string name="notify_follower">Bir takipçim</string>
<string name="notify_followed">Takip ettiğim biri</string>
<string name="notify_none">Bilgilendirme</string>
<string name="notify_favorites">Gönderimi favorilerine eklediğinde</string>
<string name="notify_follow">Beni takip ettiğinde</string>
<string name="notify_reblog">Gönderimi yinelediğinde</string>
<string name="notify_reblog">Gönderimi paylaştığında</string>
<string name="notify_mention">Benden bahsettiğinde</string>
<string name="settings_boring">Sıkıcı bölge</string>
<string name="settings_account">Hesap ayarları</string>
<string name="settings_contribute">Mastodon\'a katkıda bulunun</string>
<string name="settings_contribute">Mastodona katkıda bulunun</string>
<string name="settings_tos">Kullanım Şartları</string>
<string name="settings_privacy_policy">Gizlilik Politikası</string>
<string name="settings_spicy">Tehlikeli bölge</string>
@@ -271,7 +271,7 @@
<string name="hide_content">İçeriği gizle</string>
<string name="new_post">Yeni gönderi</string>
<string name="button_reply">Cevapla</string>
<string name="button_reblog">Yinele</string>
<string name="button_reblog">Yeniden Paylaş</string>
<string name="button_favorite">Favorile</string>
<string name="button_share">Paylaş</string>
<string name="media_no_description">ıklamasız medya</string>
@@ -282,7 +282,7 @@
<string name="home_timeline">Anasayfa</string>
<string name="my_profile">Profilim</string>
<string name="media_viewer">Medya görüntüleyici</string>
<string name="follow_user">%s\'yi takip et</string>
<string name="follow_user">%s \'yi takip et</string>
<string name="unfollowed_user">%s takip edilmedi</string>
<string name="followed_user">%s kişisini takip ediyorsunuz</string>
<string name="following_user_requested">%s takip isteği gönderdi</string>
@@ -310,11 +310,11 @@
<string name="local_timeline_info_banner">Bu gönderiler seninle aynı Mastodon sunucusunda olan kişilerin paylaştığı son gönderilerdir.</string>
<string name="dismiss">Yoksay</string>
<string name="see_new_posts">Yeni gönderileri gör</string>
<string name="load_missing_posts">Daha fazla gönderi yükle</string>
<string name="follow_back">Geri Takip Et</string>
<string name="load_missing_posts">Daha fazlası</string>
<string name="follow_back">Takip edeni Takip Et</string>
<string name="button_follow_pending">Bekliyor</string>
<string name="follows_you">Seni takip ediyor</string>
<string name="manually_approves_followers">Takipçileri manuel kabul eder</string>
<string name="manually_approves_followers">Takipçileri elle kabul et</string>
<string name="current_account">Kullanılan hesap</string>
<string name="log_out_account">%s oturumunu kapat</string>
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
@@ -331,8 +331,8 @@
<item quantity="other">%,d favori</item>
</plurals>
<plurals name="x_reblogs">
<item quantity="one">%,d yineleme</item>
<item quantity="other">%,d yineleme</item>
<item quantity="one">%,d paylaşma</item>
<item quantity="other">%,d paylaşma</item>
</plurals>
<string name="timestamp_via_app">%1$s tarihinde %2$s uygulamasıyla</string>
<string name="time_now">şimdi</string>
@@ -361,7 +361,7 @@
<string name="edit_media_added">Medya eklendi</string>
<string name="edit_media_removed">Medya kaldırıldı</string>
<string name="edit_media_reordered">Medya düzenlendi</string>
<string name="edit_marked_sensitive">Hassas olarak işarlendi</string>
<string name="edit_marked_sensitive">Hassas olarak işaretlendi</string>
<string name="edit_marked_not_sensitive">Hassas değil olarak işaretlendi</string>
<string name="edit_multiple_changed">Gönderi düzenlendi</string>
<string name="edit">Düzenle</string>
@@ -373,16 +373,16 @@
<string name="file_size_gb">%.2f GB</string>
<string name="file_upload_progress">%2$s dosyadan %1$s</string>
<string name="file_upload_time_remaining">%s kaldı</string>
<string name="upload_error_connection_lost">Cihazınızın internet bağlantısı koptu</string>
<string name="upload_error_connection_lost">Cihazınızın bağlantısı koptu</string>
<string name="upload_processing">İşleniyor…</string>
<!-- %s is version like 1.2.3 -->
<string name="update_available">Mastodon Android uygulamasının %s versiyonu indirmeye hazır.</string>
<string name="update_available">Mastodon, Android uygulamasının %s versiyonu indirmeye hazır.</string>
<!-- %s is version like 1.2.3 -->
<string name="update_ready">Mastodon Android uygulamasının %s versiyonu indirildi ve kurulmaya hazır.</string>
<string name="update_ready">Mastodon , Android uygulamasının %s versiyonu indirildi ve kurulmaya hazır.</string>
<!-- %s is file size -->
<string name="download_update">İndir (%s)</string>
<string name="install_update">Kur</string>
<string name="privacy_policy_title">Gizliliğiniz</string>
<string name="privacy_policy_title">Gizliliğiniz.</string>
<string name="privacy_policy_subtitle">Mastodon uygulaması herhangi bir veri toplama dahi katıldığınız sunucunun farklı bir politikası olabilir. \n\n %s politikası sizi tatmin etmiyorsa geri giderek farklı bir sunucu seçebilirsiniz.</string>
<string name="i_agree">Kabul ediyorum</string>
<string name="empty_list">Bu liste boş</string>
@@ -392,9 +392,9 @@
<string name="remove_bookmark">Yer İmi Kaldır</string>
<string name="bookmarks">Yer İmleri</string>
<string name="your_favorites">Favorilerin</string>
<string name="login_title">Hoşgeldin</string>
<string name="login_title">Hoşgeldiniz</string>
<string name="login_subtitle">Hesabınızı oluşturduğunuz sunucu ile giriş yapın.</string>
<string name="server_url">Sunucu URL\'si</string>
<string name="server_url">Sunucu bağlantısı</string>
<string name="signup_random_server_explain">Herhangi bir seçim yapmadan devam ederseniz dilinize göre bir sunucu seçeceğiz.</string>
<string name="server_filter_any_language">Herhangi Bir Dil</string>
<string name="server_filter_instant_signup">Koşulsuz Kayıt</string>
@@ -419,7 +419,7 @@
<string name="server_rules_disagree">Reddet</string>
<string name="privacy_policy_explanation">Kısacası: Hiçbir veri işlemiyor ya da toplamıyoruz.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">%s\'yi reddet</string>
<string name="server_policy_disagree">%s \'yi reddet</string>
<string name="profile_bio">Hakkımda</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">Hesaplar takip ediliyor…</string>
@@ -437,12 +437,12 @@
<string name="verified_link">Onaylanmış bağlantı</string>
<string name="show">Göster</string>
<string name="hide">Gizle</string>
<string name="join_default_server">%s\'e katıl</string>
<string name="join_default_server">%s katıl</string>
<string name="pick_server">Başka sunucu seç</string>
<string name="signup_or_login">veya</string>
<string name="learn_more">Daha fazla bilgi edinin</string>
<string name="learn_more">Daha fazlası</string>
<string name="welcome_to_mastodon">Mastodon\'a hoş geldiniz</string>
<string name="welcome_paragraph1">Mastodon merkezi olmayan bir sosyal ağdır, yani tek bir şirket tarafından kontrol edilmemektedir. Hepsi birbirine bağlı, bağımsız olarak işletilen birçok sunucudan oluşur.</string>
<string name="welcome_paragraph1">Mastodon , Merkezi olmayan bir sosyal ağdır, yani tek bir şirket tarafından kontrol edilmemektedir. Hepsi birbirine bağlı, bağımsız olarak işletilen birçok sunucudan oluşur.</string>
<string name="what_are_servers">Sunucular nelerdir?</string>
<string name="welcome_paragraph2"><![CDATA[Her Mastodon hesabı bir sunucuda barındırılır - her birinin kendi değerleri, kuralları ve yöneticileri vardır. Hangisini seçerseniz seçin, herhangi bir sunucudaki insanları takip edebilir ve onlarla etkileşime geçebilirsiniz.]]></string>
<string name="welcome_paragraph2"><![CDATA[Her Mastodon , hesabı bir sunucuda barındırılır - her birinin kendi değerleri, kuralları ve yöneticileri vardır. Hangisini seçerseniz seçin, herhangi bir sunucudaki insanları takip edebilir ve onlarla etkileşime geçebilirsiniz.]]></string>
</resources>

View File

@@ -288,8 +288,9 @@
<string name="sk_settings_content_types_explanation">Дозволяє налаштувати тип вмісту, наприклад, Markdown, під час написання допису. Зауважте, що не всі сервери підтримують цю функцію.</string>
<string name="sk_open_in_app">Відкрити у застосунку</string>
<string name="sk_external_share_title">Поділитися через обліковий запис</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи людей у бульбашці вашого сервера Akkoma.</string>
<string name="sk_bubble_timeline_info_banner">Це найновіші дописи з мережі керованої адміністраторами вашого сервера.</string>
<string name="sk_timeline_bubble">Бульбашка</string>
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
<string name="sk_open_in_app_failed">Не вдалося відкрити в застосунку</string>
</resources>

View File

@@ -293,4 +293,8 @@
<string name="sk_open_in_app_failed">Could not open in app</string>
<string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string>
<string name="sk_no_remote_info_hint">remote info unavailable</string>
<string name="sk_error_loading_profile">Failed loading the profile on your home instance.</string>
<string name="sk_settings_allow_remote_loading">Load info from remote instances</string>
<string name="sk_settings_allow_remote_loading_explanation">Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin.</string>
</resources>