Compare commits

..

22 Commits

Author SHA1 Message Date
sk
56a93288c4 reimplement thread ancestry 2023-06-02 19:05:18 +02:00
sk
02e3421f98 fix null pointer exception 2023-06-02 19:03:29 +02:00
sk
1ce49c68fe bump version 2023-06-02 01:45:55 +02:00
sk
d37e880993 don't close sheet after logging out 2023-06-02 01:45:30 +02:00
sk
6fdb81a01f Merge remote-tracking branch 'upstream/l10n_master' 2023-06-02 01:37:14 +02:00
ihor_ck
f9d6827572 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-01 23:36:24 +00:00
Linerly
10bf72b9ff Translated using Weblate (Indonesian)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-01 23:36:24 +00:00
Choukajohn
800f929a15 Translated using Weblate (French)
Currently translated at 100.0% (292 of 292 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-01 23:36:24 +00:00
sk
bfcff1e19f fix null pointer exception
closes sk22#539
2023-06-02 01:34:31 +02:00
sk
f373e7df3e put code in method, add todo 2023-06-02 01:16:21 +02:00
sk
3985de5b14 visually connect descendant replies in threads
closes sk22#256
closes sk22#510
2023-06-02 00:55:42 +02:00
sk
e175a721d4 remove additional padding with translate button 2023-06-02 00:17:50 +02:00
sk
d9784ebc31 use /about web uri for akkoma 2023-06-01 19:28:46 +02:00
sk
f241092277 use isInstanceAkkoma() 2023-06-01 19:22:01 +02:00
sk
0702703d78 normalize instance uri 2023-06-01 19:13:03 +02:00
sk
2c4504bad3 fix current fragment detection 2023-06-01 19:12:50 +02:00
Eugen Rochko
07ca5a8b77 New translations strings.xml (Bengali) 2023-06-01 19:07:10 +02:00
sk
798a43906f denser account switcher
this one's for @experiencersinternational
2023-06-01 18:46:53 +02:00
sk
41cb0f2e09 implement assist url in instance rules 2023-06-01 18:38:45 +02:00
sk
e12c0fb81f bump version 2023-06-01 18:10:30 +02:00
Eugen Rochko
6926a212f4 New translations strings.xml (Bengali) 2023-06-01 17:46:40 +02:00
Eugen Rochko
936f39161b New translations strings.xml (German) 2023-05-31 20:11:48 +02:00
27 changed files with 454 additions and 76 deletions

View File

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

View File

@@ -0,0 +1,89 @@
package org.joinmastodon.android.fragments;
import static org.junit.Assert.*;
import android.util.Pair;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.junit.Test;
import java.util.List;
import java.util.stream.Collectors;
public class ThreadFragmentTest {
private Status fakeStatus(String id, String inReplyTo) {
Status status = Status.ofFake(id, null, null);
status.inReplyToId = inReplyTo;
return status;
}
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
ThreadFragment.NeighborAncestryInfo info = new ThreadFragment.NeighborAncestryInfo(s);
info.descendantNeighbor = d;
info.ancestoringNeighbor = a;
return info;
}
@Test
public void mapNeighborhoodAncestry() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
Status mainStatus = fakeStatus("main status", "younger ancestor");
context.descendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
List<ThreadFragment.NeighborAncestryInfo> neighbors =
ThreadFragment.mapNeighborhoodAncestry(mainStatus, context);
assertEquals(List.of(
fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null),
fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)),
fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)),
fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus),
fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)),
fakeInfo(context.descendants.get(2), null, context.descendants.get(1)),
fakeInfo(context.descendants.get(3), null, null)
), neighbors);
}
@Test
public void sortStatusContext() {
StatusContext context = new StatusContext();
context.ancestors = List.of(
fakeStatus("younger ancestor", "oldest ancestor"),
fakeStatus("oldest ancestor", null)
);
context.descendants = List.of(
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("first reply", "main status"),
fakeStatus("another reply", "main status")
);
ThreadFragment.sortStatusContext(
fakeStatus("main status", "younger ancestor"),
context
);
List<Status> expectedAncestors = List.of(
fakeStatus("oldest ancestor", null),
fakeStatus("younger ancestor", "oldest ancestor")
);
List<Status> expectedDescendants = List.of(
fakeStatus("first reply", "main status"),
fakeStatus("reply to first reply", "first reply"),
fakeStatus("third level reply", "reply to first reply"),
fakeStatus("another reply", "main status")
);
// TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect
}
}

View File

@@ -18,6 +18,7 @@ import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@@ -29,8 +30,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){
@@ -40,8 +41,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
openComposeFragment(sessions.get(0).getID());
}else{
new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
if (open) {
UiUtils.lookupURL(this, accountId, text, false, (clazz, args) -> {
if (open && text.isPresent()) {
UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> {
if (clazz == null) {
finish();
return;

View File

@@ -8,6 +8,8 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
@@ -28,8 +30,6 @@ import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
private Fragment currentFragment;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
@@ -200,15 +200,20 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}
}
@Override
public void showFragment(Fragment fragment) {
super.showFragment(fragment);
this.currentFragment = fragment;
public Fragment getCurrentFragment() {
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
FrameLayout fl = fragmentContainers.get(i);
if (fl.getVisibility() == View.VISIBLE) {
return getFragmentManager().findFragmentById(fl.getId());
}
}
return null;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
super.onProvideAssistContent(assistContent);
callFragmentToProvideAssistContent(currentFragment, assistContent);
Fragment fragment = getCurrentFragment();
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
}
}

View File

@@ -159,7 +159,7 @@ public class CacheController{
}
}
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isPleroma())
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){

View File

@@ -99,7 +99,7 @@ public class AccountSession{
public Uri getInstanceUri() {
return new Uri.Builder()
.scheme("https")
.authority(domain)
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
.build();
}
}

View File

@@ -16,6 +16,7 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
@@ -26,7 +27,6 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent;
@@ -132,7 +132,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.clear();
}
protected void prependItems(List<T> items, boolean notify){
protected int prependItems(List<T> items, boolean notify){
data.addAll(0, items);
int offset=0;
for(T s:items){
@@ -145,6 +145,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
return offset;
}
protected String getMaxID(){
@@ -205,7 +206,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
if(holder!=null && list!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
int[] pos={0, 0};
@@ -337,6 +338,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
@@ -348,18 +351,40 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor();
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor() && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
}
}
}
}
}
// shifting the selection box down
// see also: FooterStatusDisplayItem#onBind (setMargins)
if (isWarning || firstIndex < 0 || lastIndex < 0) return;
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(prevIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(nextIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
}
});
list.setItemAnimator(new BetterItemAnimator());
@@ -772,6 +797,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor()) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}

View File

@@ -1084,7 +1084,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
req.status=text;
req.localOnly=localOnly;
req.visibility=localOnly && instance.isPleroma() ? StatusPrivacy.LOCAL : statusVisibility;
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.contentType=contentType;
@@ -1899,7 +1899,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.isPleroma()) {
if (instance.isAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@@ -14,7 +14,7 @@ public interface HasAccountID {
}
default boolean isInstanceAkkoma() {
return getInstance().map(Instance::isPleroma).orElse(false);
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default Optional<Instance> getInstance() {

View File

@@ -78,7 +78,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isPleroma)
.map(Instance::isAkkoma)
.orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
@@ -310,7 +310,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isPleroma())
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {

View File

@@ -163,8 +163,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (AccountSessionManager.getInstance().getAccount(accountID).getInstance().map(Instance::isPleroma).orElse(false))
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
}
});

View File

@@ -184,12 +184,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true;
if(!isOwnProfile)
loadRelationship();
else {
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(domain);
if (instance != null && instance.isPleroma()) {
maxFields = instance.pleroma.metadata.fieldsLimits.maxFields;
}
}
else if (isInstanceAkkoma() && getInstance().isPresent())
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))

View File

@@ -225,7 +225,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
if (instance.map(Instance::isPleroma).orElse(false)) {
if (isInstanceAkkoma()) {
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.reply_visibility);
@@ -374,7 +374,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
glitchModeItem.enabled = i.checked;
if (i.checked) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (!instance.map(Instance::isPleroma).orElse(false)) {
if (!isInstanceAkkoma()) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
}
} else {
@@ -744,7 +744,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/settings").build();
return base.path(isInstanceAkkoma() ? "/about" : "/settings").build();
}
@Override

View File

@@ -2,38 +2,66 @@ package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.View;
import androidx.annotation.NonNull;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import me.grishka.appkit.api.SimpleCallback;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus;
/**
* lists the hierarchy of ancestors and descendants in a thread. level 0 = the main status.
* e.g.
* <pre>
* [0] ancestor: -2 ↰
* [1] ancestor: -1 ↰
* [2] main status: 0 ↰
* [3] descendant: 1 ↰
* [4] descendant: 2 ↰
* [5] descendant: 3
* [6] descendant: 1
* [7] descendant: 1 ↰
* [8] descendant: 2
* </pre>
* confused? good. /j
*/
private final List<Pair<String, Integer>> levels = new ArrayList<>();
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -49,13 +77,42 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
// modifying hidden filtered items if status is displayed as a warning
List<StatusDisplayItem> itemsToModify =
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
? warning.filteredItems
: items;
for(int i = 0; i < itemsToModify.size(); i++){
StatusDisplayItem item = itemsToModify.get(i);
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
if (ancestryInfo != null) {
item.setAncestryInfo(
ancestryInfo,
s.id.equals(mainStatus.id),
ancestryInfo.getAncestoringNeighbor()
.map(ancestor -> ancestor.id.equals(mainStatus.id))
.orElse(false)
);
}
if (item instanceof ReblogOrReplyLineStatusDisplayItem && !item.isDirectDescendant) {
deleteTheseItems.add(i);
}
if(s.id.equals(mainStatus.id)){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
return items;
@@ -70,36 +127,22 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
if (getActivity() == null) return;
if(refreshing){
data.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.isPleroma()){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=result.ancestors.size()-1; i >= 0; i--){
Status s=result.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
result.descendants=getDescendantsOrdered(mainStatus.id,
result.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
// TODO: figure out how this code works
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
@@ -108,7 +151,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
prependItems(result.ancestors, !refreshing);
int prependedCount = prependItems(result.ancestors, !refreshing);
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
displayItems.remove(prependedCount);
adapter.notifyItemRemoved(prependedCount);
count--;
}
dataLoaded();
if(refreshing){
refreshDone();
@@ -120,7 +168,61 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
.exec(accountID);
}
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
List<Status> statuses = new ArrayList<>(context.ancestors);
statuses.add(mainStatus);
statuses.addAll(context.descendants);
int count = statuses.size();
for (int index = 0; index < count; index++) {
Status current = statuses.get(index);
NeighborAncestryInfo item = new NeighborAncestryInfo(current);
item.descendantNeighbor = Optional
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
.filter(s -> s.inReplyToId.equals(current.id))
.orElse(null);
item.ancestoringNeighbor = Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
.filter(ancestor -> ancestor
.getDescendantNeighbor()
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
.orElse(false))
.flatMap(NeighborAncestryInfo::getStatus)
.orElse(null);
ancestry.add(item);
}
return ancestry;
}
public static void sortStatusContext(Status mainStatus, StatusContext context) {
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:context.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=context.ancestors.size()-1; i >= 0; i--){
Status s=context.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
context.descendants=getDescendantsOrdered(mainStatus.id,
context.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
List<Status> out=new ArrayList<>();
for(Status s:getDirectDescendants(id, statuses)){
out.add(s);
@@ -132,7 +234,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
return out;
}
private List<Status> getDirectDescendants(String id, List<Status> statuses){
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
return statuses.stream()
.filter(s -> s.inReplyToId.equals(id))
.collect(Collectors.toList());
@@ -194,4 +296,47 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(mainStatus.url);
}
public static class NeighborAncestryInfo {
protected Status status, descendantNeighbor, ancestoringNeighbor;
public NeighborAncestryInfo(@NonNull Status status) {
this.status = status;
}
public Optional<Status> getStatus() {
return Optional.ofNullable(status);
}
public Optional<Status> getDescendantNeighbor() {
return Optional.ofNullable(descendantNeighbor);
}
public Optional<Status> getAncestoringNeighbor() {
return Optional.ofNullable(ancestoringNeighbor);
}
public boolean hasDescendantNeighbor() {
return getDescendantNeighbor().isPresent();
}
public boolean hasAncestoringNeighbor() {
return getAncestoringNeighbor().isPresent();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
return status.equals(that.status)
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
}
@Override
public int hashCode() {
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
}
}
}

View File

@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
@@ -24,6 +26,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
@@ -38,7 +41,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{
public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -130,6 +133,15 @@ public class InstanceRulesFragment extends ToolbarFragment{
}
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(new Uri.Builder()
.scheme("https")
.authority(instance.normalizedUri)
.path("/about")
.build());
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull

View File

@@ -11,6 +11,7 @@ import java.net.IDN;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Parcel
public class Instance extends BaseModel{
@@ -88,6 +89,9 @@ public class Instance extends BaseModel{
public PleromaPollLimits pollLimits;
/** like uri, but always without scheme and trailing slash */
public transient String normalizedUri;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@@ -97,6 +101,10 @@ public class Instance extends BaseModel{
rules=Collections.emptyList();
if(shortDescription==null)
shortDescription="";
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
normalizedUri = uri
.replaceFirst("^https://", "")
.replaceFirst("/$", "");
}
@Override
@@ -136,10 +144,26 @@ public class Instance extends BaseModel{
return ci;
}
public boolean isPleroma() {
public boolean isAkkoma() {
return pleroma != null;
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)
.map(m -> m.features);
return switch (feature) {
case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline"))
.orElse(false);
};
}
public enum Feature {
BUBBLE_TIMELINE
}
@Parcel
public static class Rule{
public String id;

View File

@@ -259,13 +259,13 @@ public class TimelineDefinition {
public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().map(Instance::isPleroma).orElse(false);
return session.getInstance().map(Instance::isAkkoma).orElse(false);
}
@Override
public boolean wantsDefault(AccountSession session) {
return session.getInstance()
.map(i -> i.isPleroma() && i.pleroma.metadata.features.contains("bubble_timeline"))
.map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
.orElse(false);
}
};

View File

@@ -63,6 +63,7 @@ public class AccountSwitcherSheet extends BottomSheet{
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
private AccountsAdapter accountsAdapter;
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false, null);
@@ -101,10 +102,10 @@ public class AccountSwitcherSheet extends BottomSheet{
setOnDismissListener((d) -> activity.finish());
}
adapter.addAdapter(new AccountsAdapter());
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
if (!externalShare) {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_add_24px), () -> {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
Nav.go(activity, CustomWelcomeFragment.class, null);
dismiss();
}));
@@ -201,7 +202,10 @@ public class AccountSwitcherSheet extends BottomSheet{
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
} else {
dismiss();
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
accounts.remove(w);
});
}
}

View File

@@ -134,12 +134,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
reply.setSelected(item.status.repliesCount > 0);
// 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;
reply.setSelected(item.status.repliesCount > compareTo);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor() &&
!nextIsWarning;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){

View File

@@ -10,7 +10,6 @@ import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -22,7 +21,6 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
@@ -49,6 +47,32 @@ public abstract class StatusDisplayItem{
public final BaseStatusListFragment parentFragment;
public boolean inset;
public int index;
private ThreadFragment.NeighborAncestryInfo ancestryInfo;
public boolean
isMainStatus = true,
isDirectDescendant = false;
public boolean hasDescendantNeighbor() {
return Optional.ofNullable(ancestryInfo)
.map(ThreadFragment.NeighborAncestryInfo::hasDescendantNeighbor)
.orElse(false);
}
public boolean hasAncestoringNeighbor() {
return Optional.ofNullable(ancestryInfo)
.map(ThreadFragment.NeighborAncestryInfo::hasAncestoringNeighbor)
.orElse(false);
}
public void setAncestryInfo(
ThreadFragment.NeighborAncestryInfo ancestryInfo,
boolean isMainStatus,
boolean isDirectDescendant
) {
this.ancestryInfo = ancestryInfo;
this.isMainStatus = isMainStatus;
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;

View File

@@ -237,6 +237,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int pos = getAbsoluteAdapterPosition();
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
(translateVisible &&
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
? 0 : V.dp(12)
);
if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams);
readMore.setVisibility(View.GONE);

View File

@@ -9,7 +9,8 @@
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp"
android:layout_alignParentStart="true"
android:layout_centerInParent="true"
android:importantForAccessibility="no"/>

View File

@@ -88,6 +88,7 @@
<item quantity="one">%d জন ব্যক্তি বলছেন</item>
<item quantity="other">%d jon ব্যক্তিরা বলছেন</item>
</plurals>
<string name="sending_report">রিপোর্ট পাঠানো হচ্ছে…</string>
<!-- %s is the email address -->
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
<!-- %s is version like 1.2.3 -->
@@ -96,4 +97,8 @@
<!-- %s is server domain -->
<!-- Shown in a progress dialog when you tap "follow all" -->
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="welcome_to_mastodon">Mastodon - এ আপনাকে স্বাগত জানাই</string>
<string name="welcome_paragraph1">Mastodon হল একটি বিকেন্দ্রীভূত সামাজিক নেটওয়ার্ক, যার মানে কোনো একক কোম্পানি এটিকে নিয়ন্ত্রণ করে না। এটি অনেকগুলি স্বাধীনভাবে চালিত সার্ভারের সমন্বয়ে গঠিত, যেখানে সব সার্ভারগুলি একসাথে সংযুক্ত৷</string>
<string name="what_are_servers">সার্ভার কি?</string>
<string name="welcome_paragraph2"><![CDATA[প্রতিটি Mastodon অ্যাকাউন্টকে একটি সার্ভারে হোস্ট করা হয় — প্রত্যেকটির নিজস্ব মান, নিয়ম এবং প্রশাসক (অ্যাডমিন) রয়েছে। আপনি যে কোনো সার্ভারই বেছে নিন না কেন তা বিবেচ্য নয়, আপনি যেকোনো সার্ভারের লোকেদের সাথে যোগাযোগ করতে এবং তাদের ফলো করতে পারেন।]]></string>
</resources>

View File

@@ -438,8 +438,11 @@
<string name="show">Anzeigen</string>
<string name="hide">Ausblenden</string>
<string name="join_default_server">%s beitreten</string>
<string name="pick_server">Wähle einen anderen Server</string>
<string name="signup_or_login">oder</string>
<string name="learn_more">Mehr erfahren</string>
<string name="welcome_to_mastodon">Willkommen auf Mastodon</string>
<string name="welcome_paragraph1">Mastodon ist ein dezentrales, soziales Netzwerk. Das bedeutet, dass es nicht von einem einzigen Unternehmen kontrolliert wird. Das Netzwerk besteht aus unabhängig voneinander betriebenen Servern, die miteinander verbunden sind.</string>
<string name="what_are_servers">Was sind Server?</string>
<string name="welcome_paragraph2"><![CDATA[Jedes Mastodon-Konto wird auf einem Server gehostet. Jeder Server hat dabei seine eigenen Werte, Regeln und Administrator*innen. Aber egal, für welchen Server Du Dich entscheidest: Du kannst mit Leuten von anderen Servern interagieren und ihnen folgen.]]></string>
</resources>

View File

@@ -287,4 +287,10 @@
<string name="sk_content_type_unspecified">Non spécifié</string>
<string name="sk_settings_content_types_explanation">Permet de définir un type de contenu comme Markdown lors de la création d\'un message. Gardez à l\'esprit que toutes les instances ne le prennent pas en charge.</string>
<string name="sk_settings_default_content_type">Type de contenu par défaut</string>
<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_timeline_bubble">Bulle</string>
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
</resources>

View File

@@ -287,4 +287,10 @@
<string name="sk_content_type">Jenis konten</string>
<string name="sk_content_type_unspecified">Tidak ditentukan</string>
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yamg paling terkini oleh orang-orang dalam gelembung server Akkoma Anda.</string>
<string name="sk_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</string>
<string name="sk_external_share_or_open_title">Bagikan atau buka dengan akun</string>
</resources>

View File

@@ -286,4 +286,10 @@
<string name="sk_settings_default_content_type_explanation">Це дозволяє вам попередньо вибрати тип вмісту під час написання нових дописів, замінивши значення, встановлене в «Налаштуваннях постингу».</string>
<string name="sk_content_type_markdown">Markdown</string>
<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_timeline_bubble">Бульбашка</string>
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
</resources>