Merge branch 'develop' into main

pull/254/head 3.0.6
Thomas 2 years ago
commit 867d2ed492

2
.gitignore vendored

@ -10,3 +10,5 @@
local.properties
/cropper/build/
/build/
/app/fdroid/release/
/app/playstore/release/

@ -9,8 +9,8 @@ android {
defaultConfig {
minSdk 21
targetSdk 31
versionCode 395
versionName "3.0.5"
versionCode 396
versionName "3.0.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
flavorDimensions "default"

@ -2,7 +2,7 @@
<paths>
<external-path
name="my_images"
path="Android/data/fr.gouv.etalab.mastodon.test/files/Pictures" />
path="Android/data/fr.gouv.etalab.mastodon/files/Pictures" />
<cache-path
name="*"

@ -117,6 +117,11 @@
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppThemeBar"
android:label="@string/search" />
<activity
android:name=".activities.TrendsActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/trending"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.ReorderTimelinesActivity"
android:configChanges="keyboardHidden|orientation|screenSize"

@ -107,6 +107,7 @@ import app.fedilab.android.activities.ReorderTimelinesActivity;
import app.fedilab.android.activities.ScheduledActivity;
import app.fedilab.android.activities.SearchResultTabActivity;
import app.fedilab.android.activities.SettingsActivity;
import app.fedilab.android.activities.TrendsActivity;
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
import app.fedilab.android.client.entities.api.Emoji;
import app.fedilab.android.client.entities.api.EmojiInstance;
@ -509,6 +510,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
} else if (id == R.id.nav_announcements) {
Intent intent = new Intent(this, AnnouncementActivity.class);
startActivity(intent);
} else if (id == R.id.nav_trends) {
Intent intent = new Intent(this, TrendsActivity.class);
startActivity(intent);
} else if (id == R.id.nav_cache) {
Intent intent = new Intent(BaseMainActivity.this, CacheActivity.class);
startActivity(intent);
@ -900,10 +904,12 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
itemFilter.setTitle(show_filtered);
}
popup.setOnDismissListener(menu1 -> {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
if (binding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
}
}
});
String finalShow_filtered = show_filtered;
@ -990,13 +996,15 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
}
public void refreshFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem());
if (fragment instanceof FragmentNotificationContainer) {
FragmentTransaction fragTransaction = getSupportFragmentManager().beginTransaction();
fragTransaction.detach(fragment).commit();
FragmentTransaction fragTransaction2 = getSupportFragmentManager().beginTransaction();
fragTransaction2.attach(fragment);
fragTransaction2.commit();
if (binding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentNotificationContainer) {
FragmentTransaction fragTransaction = getSupportFragmentManager().beginTransaction();
fragTransaction.detach(fragment).commit();
FragmentTransaction fragTransaction2 = getSupportFragmentManager().beginTransaction();
fragTransaction2.attach(fragment);
fragTransaction2.commit();
}
}
}
@ -1035,16 +1043,18 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
*/
private void scrollToTop() {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonConversation) {
FragmentMastodonConversation fragmentMastodonConversation = ((FragmentMastodonConversation) fragment);
fragmentMastodonConversation.scrollToTop();
} else if (fragment instanceof FragmentNotificationContainer) {
FragmentNotificationContainer fragmentNotificationContainer = ((FragmentNotificationContainer) fragment);
fragmentNotificationContainer.scrollToTop();
if (binding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonConversation) {
FragmentMastodonConversation fragmentMastodonConversation = ((FragmentMastodonConversation) fragment);
fragmentMastodonConversation.scrollToTop();
} else if (fragment instanceof FragmentNotificationContainer) {
FragmentNotificationContainer fragmentNotificationContainer = ((FragmentNotificationContainer) fragment);
fragmentNotificationContainer.scrollToTop();
}
}
}

@ -75,7 +75,7 @@ public class MainApplication extends MultiDexApplication {
super.attachBaseContext(base);
MultiDex.install(MainApplication.this);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MainApplication.this);
boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), true);
boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), false);
if (send_crash_reports) {
ACRA.init(this, new CoreConfigurationBuilder()
//core configuration:

@ -109,12 +109,17 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
@Override
public void onReceive(android.content.Context context, Intent intent) {
String imgpath = intent.getStringExtra("imgpath");
float focusX = intent.getFloatExtra("focusX", -2);
float focusY = intent.getFloatExtra("focusY", -2);
if (imgpath != null) {
int position = 0;
for (Status status : statusList) {
if (status.media_attachments != null && status.media_attachments.size() > 0) {
for (Attachment attachment : status.media_attachments) {
if (attachment.local_path.equalsIgnoreCase(imgpath)) {
if (focusX != -2) {
attachment.focus = focusX + "," + focusY;
}
composeAdapter.notifyItemChanged(position);
break;
}

@ -82,7 +82,7 @@ public class ContextActivity extends BaseActivity {
focusedStatus = null; // or other values
if (b != null)
focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS);
if (focusedStatus == null) {
if (focusedStatus == null && currentAccount == null || currentAccount.mastodon_account == null) {
finish();
return;
}

@ -33,15 +33,18 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@ -94,6 +97,7 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
private float startX;
private float startY;
private ActivityMediaPagerBinding binding;
private FragmentMedia mCurrentFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -123,7 +127,7 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
setTitle("");
ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(MediaActivity.this);
ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.mediaViewpager.setAdapter(mPagerAdapter);
binding.mediaViewpager.setSaveEnabled(false);
binding.mediaViewpager.setCurrentItem(mediaPosition - 1);
@ -135,15 +139,14 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
binding.mediaDescription.setText(description);
}
binding.mediaViewpager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
binding.mediaViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
public void onPageScrollStateChanged(int state) {
}
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
String description = attachments.get(position).description;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
@ -153,13 +156,9 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
binding.mediaDescription.setText(description);
}
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
setFullscreen(true);
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
@ -358,18 +357,22 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
public FragmentMedia getCurrentFragment() {
return mCurrentFragment;
}
/**
* Media Pager
*/
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
ScreenSlidePagerAdapter(FragmentActivity fa) {
super(fa);
@SuppressWarnings("deprecation")
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@NonNull
@NotNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
FragmentMedia mediaSliderFragment = new FragmentMedia();
bundle.putInt(Helper.ARG_MEDIA_POSITION, position);
@ -379,7 +382,15 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface {
}
@Override
public int getItemCount() {
public void setPrimaryItem(@NotNull ViewGroup container, int position, @NotNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((FragmentMedia) object);
}
super.setPrimaryItem(container, position, object);
}
@Override
public int getCount() {
return attachments.size();
}
}

@ -32,7 +32,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
@ -63,7 +62,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.Date;
@ -94,6 +93,7 @@ import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.FieldAdapter;
import app.fedilab.android.ui.drawer.IdentityProofsAdapter;
import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
@ -247,29 +247,27 @@ public class ProfileActivity extends BaseActivity {
binding.accountTabLayout.clearOnTabSelectedListeners();
binding.accountTabLayout.removeAllTabs();
//Tablayout for timelines/following/followers
FedilabProfileTLPageAdapter fedilabProfileTLPageAdapter = new FedilabProfileTLPageAdapter(ProfileActivity.this, account);
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab());
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab());
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab());
FedilabProfileTLPageAdapter fedilabProfileTLPageAdapter = new FedilabProfileTLPageAdapter(getSupportFragmentManager(), account);
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.status_cnt, Helper.withSuffix(account.statuses_count))));
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.following_cnt, Helper.withSuffix(account.following_count))));
binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.followers_cnt, Helper.withSuffix(account.followers_count))));
binding.accountViewpager.setAdapter(fedilabProfileTLPageAdapter);
binding.accountViewpager.setOffscreenPageLimit(3);
binding.accountViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.accountTabLayout));
binding.accountTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
binding.accountViewpager.setCurrentItem(tab.getPosition());
}
new TabLayoutMediator(binding.accountTabLayout, binding.accountViewpager,
(tab, position) -> {
binding.accountViewpager.setCurrentItem(tab.getPosition(), true);
switch (position) {
case 0:
tab.setText(getString(R.string.status_cnt, Helper.withSuffix(account.statuses_count)));
break;
case 1:
tab.setText(getString(R.string.following_cnt, Helper.withSuffix(account.following_count)));
break;
case 2:
tab.setText(getString(R.string.followers_cnt, Helper.withSuffix(account.followers_count)));
break;
}
}
).attach();
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
binding.accountTabLayout.setTabTextColors(ThemeHelper.getAttColor(ProfileActivity.this, R.attr.mTextColor), ContextCompat.getColor(ProfileActivity.this, R.color.cyanea_accent_dark_reference));
binding.accountTabLayout.setTabIconTint(ThemeHelper.getColorStateList(ProfileActivity.this));
boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false);
@ -357,43 +355,9 @@ public class ProfileActivity extends BaseActivity {
//Fields for profile
List<Field> fields = account.fields;
if (fields != null && fields.size() > 0) {
for (int i = 0; i < fields.size(); i++) {
LinearLayout field;
TextView labelView;
TextView valueView;
switch (i) {
case 1:
field = binding.field1;
labelView = binding.label1;
valueView = binding.value1;
break;
case 2:
field = binding.field2;
labelView = binding.label2;
valueView = binding.value2;
break;
case 3:
field = binding.field3;
labelView = binding.label3;
valueView = binding.value3;
break;
default:
field = binding.field4;
labelView = binding.label4;
valueView = binding.value4;
break;
}
field.setVisibility(View.VISIBLE);
if (fields.get(i).verified_at != null) {
valueView.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(ProfileActivity.this, R.drawable.ic_baseline_verified_24), null);
fields.get(i).value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(ProfileActivity.this, R.color.verified_text)), 0, fields.get(i).value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
valueView.setText(fields.get(i).value_span != null ? fields.get(i).value_span : fields.get(i).value, TextView.BufferType.SPANNABLE);
valueView.setMovementMethod(LinkMovementMethod.getInstance());
labelView.setText(fields.get(i).name);
}
binding.fieldsContainer.setVisibility(View.VISIBLE);
FieldAdapter fieldAdapter = new FieldAdapter(fields);
binding.fieldsContainer.setAdapter(fieldAdapter);
binding.fieldsContainer.setLayoutManager(new LinearLayoutManager(ProfileActivity.this));
}
if (account.span_display_name == null && account.display_name == null) {
binding.accountDn.setText(account.username);

@ -24,7 +24,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.ContextCompat;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.tabs.TabLayout;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityScheduledBinding;
@ -56,31 +56,30 @@ public class ScheduledActivity extends BaseActivity {
MastodonHelper.loadPPMastodon(binding.profilePicture, currentAccount.mastodon_account);
binding.title.setText(R.string.scheduled);
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab());
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab());
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab());
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_server)));
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_client)));
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.reblog)));
binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(ScheduledActivity.this));
binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(getSupportFragmentManager()));
binding.scheduleViewpager.setOffscreenPageLimit(3);
binding.scheduleViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.scheduleTablayout));
binding.scheduleTablayout.setTabTextColors(ThemeHelper.getAttColor(ScheduledActivity.this, R.attr.mTextColor), ContextCompat.getColor(ScheduledActivity.this, R.color.cyanea_accent_dark_reference));
binding.scheduleTablayout.setTabIconTint(ThemeHelper.getColorStateList(ScheduledActivity.this));
new TabLayoutMediator(binding.scheduleTablayout, binding.scheduleViewpager,
(tab, position) -> {
binding.scheduleViewpager.setCurrentItem(tab.getPosition(), true);
switch (position) {
case 0:
tab.setText(getString(R.string.toots_server));
break;
case 1:
tab.setText(getString(R.string.toots_client));
break;
case 2:
tab.setText(getString(R.string.reblog));
break;
}
}
).attach();
binding.scheduleTablayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
binding.scheduleViewpager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
@Override

@ -21,6 +21,7 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
@ -28,11 +29,12 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.jetbrains.annotations.NotNull;
@ -75,35 +77,12 @@ public class SearchResultTabActivity extends BaseActivity {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
}
setTitle(search);
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab());
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab());
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab());
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab());
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.accounts)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.action_cache)));
binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(SearchResultTabActivity.this, R.attr.mTextColor), ContextCompat.getColor(SearchResultTabActivity.this, R.color.cyanea_accent_dark_reference));
binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(SearchResultTabActivity.this));
ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(SearchResultTabActivity.this);
binding.searchViewpager.setAdapter(mPagerAdapter);
binding.searchViewpager.setSaveEnabled(false);
binding.searchViewpager.setOffscreenPageLimit(3);
new TabLayoutMediator(binding.searchTabLayout, binding.searchViewpager,
(tab, position) -> {
binding.searchViewpager.setCurrentItem(tab.getPosition(), true);
switch (position) {
case 0:
tab.setText(getString(R.string.tags));
break;
case 1:
tab.setText(getString(R.string.accounts));
break;
case 2:
tab.setText(getString(R.string.toots));
break;
case 3:
tab.setText(getString(R.string.action_cache));
break;
}
}
).attach();
binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
@ -112,21 +91,22 @@ public class SearchResultTabActivity extends BaseActivity {
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.searchViewpager.getCurrentItem());
if (fragment instanceof FragmentMastodonAccount) {
FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment);
fragmentMastodonAccount.scrollToTop();
} else if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonTag) {
FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment);
fragmentMastodonTag.scrollToTop();
Fragment fragment;
if (binding.searchViewpager.getAdapter() != null) {
fragment = (Fragment) binding.searchViewpager.getAdapter().instantiateItem(binding.searchViewpager, tab.getPosition());
if (fragment instanceof FragmentMastodonAccount) {
FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment);
fragmentMastodonAccount.scrollToTop();
} else if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonTag) {
FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment);
fragmentMastodonTag.scrollToTop();
}
}
}
});
@ -149,7 +129,7 @@ public class SearchResultTabActivity extends BaseActivity {
imm.hideSoftInputFromWindow(binding.searchTabLayout.getWindowToken(), 0);
query = query.replaceAll("^#+", "");
search = query.trim();
ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(SearchResultTabActivity.this);
ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.searchViewpager.setAdapter(mPagerAdapter);
searchView.clearFocus();
setTitle(search);
@ -172,7 +152,24 @@ public class SearchResultTabActivity extends BaseActivity {
searchView.setIconified(false);
});
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.searchViewpager.setAdapter(mPagerAdapter);
binding.searchViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position);
if (tab != null)
tab.select();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
return true;
}
@ -193,16 +190,15 @@ public class SearchResultTabActivity extends BaseActivity {
/**
* Pager adapter for the 4 fragments
*/
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
ScreenSlidePagerAdapter(FragmentActivity fa) {
super(fa);
@SuppressWarnings("deprecation")
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@NonNull
@NotNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
switch (position) {
case 0:
@ -229,7 +225,11 @@ public class SearchResultTabActivity extends BaseActivity {
}
@Override
public int getItemCount() {
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
}
@Override
public int getCount() {
return 4;
}
}

@ -0,0 +1,156 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.jetbrains.annotations.NotNull;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.ActivityTrendsBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
public class TrendsActivity extends BaseActivity {
private ActivityTrendsBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityTrendsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
}
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots)));
binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(TrendsActivity.this, R.attr.mTextColor), ContextCompat.getColor(TrendsActivity.this, R.color.cyanea_accent_dark_reference));
binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(TrendsActivity.this));
binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
binding.trendsViewpager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
Fragment fragment;
if (binding.trendsViewpager.getAdapter() != null) {
fragment = (Fragment) binding.trendsViewpager.getAdapter().instantiateItem(binding.trendsViewpager, tab.getPosition());
if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonTag) {
FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment);
fragmentMastodonTag.scrollToTop();
}
}
}
});
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.trendsViewpager.setAdapter(mPagerAdapter);
binding.trendsViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position);
if (tab != null)
tab.select();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
@Override
public boolean onOptionsItemSelected(@NotNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Pager adapter for the 4 fragments
*/
@SuppressWarnings("deprecation")
private static class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@NotNull
@Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
if (position == 0) {
FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_TAG);
fragmentMastodonTag.setArguments(bundle);
return fragmentMastodonTag;
}
FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_MESSAGE);
fragmentMastodonTimeline.setArguments(bundle);
return fragmentMastodonTimeline;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
}
@Override
public int getCount() {
return 2;
}
}
}

@ -21,6 +21,7 @@ import app.fedilab.android.client.entities.api.Conversation;
import app.fedilab.android.client.entities.api.Marker;
import app.fedilab.android.client.entities.api.MastodonList;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.api.Tag;
import app.fedilab.android.client.entities.misskey.MisskeyNote;
import app.fedilab.android.client.entities.nitter.Nitter;
import app.fedilab.android.client.entities.peertube.PeertubeVideo;
@ -52,6 +53,14 @@ public interface MastodonTimelinesService {
@Query("limit") Integer limit
);
@GET("trends/statuses")
Call<List<Status>> getStatusTrends(@Header("Authorization") String token);
@GET("trends/tags")
Call<List<Tag>> getTagTrends(@Header("Authorization") String token);
//Public Tags timelines
@GET("timelines/tag/{hashtag}")
Call<List<Status>> getHashTag(

@ -43,7 +43,37 @@ public class Attachment implements Serializable {
public long size;
@SerializedName("local_path")
public String local_path;
@SerializedName("meta")
public Meta meta;
public String peertubeHost = null;
public String peertubeId = null;
public String focus = null;
public static class Meta implements Serializable {
@SerializedName("focus")
public Focus focus;
@SerializedName("original")
public MediaData original;
@SerializedName("small")
public MediaData small;
}
public static class Focus implements Serializable {
@SerializedName("x")
public float x;
@SerializedName("y")
public float y;
}
public static class MediaData implements Serializable {
@SerializedName("width")
public int width;
@SerializedName("height")
public int height;
@SerializedName("size")
public String size;
@SerializedName("aspect")
public float aspect;
}
}

@ -309,7 +309,7 @@ public class QuickLoad {
QuickLoad localQuickLoad = getSavedValue(account, Timeline.TimeLineEnum.LOCAL, null);
QuickLoad publicQuickLoad = getSavedValue(account, Timeline.TimeLineEnum.PUBLIC, null);
if (homeQuickLoad != null && homeQuickLoad.statuses != null) {
if (homeQuickLoad != null && homeQuickLoad.statuses != null && newStatus != null) {
for (int i = 0; i < homeQuickLoad.statuses.size(); i++) {
if (homeQuickLoad.statuses.get(i).id.equals(newStatus.id)) {
homeQuickLoad.statuses.set(i, newStatus);
@ -327,7 +327,7 @@ public class QuickLoad {
e.printStackTrace();
}
}
if (localQuickLoad != null && localQuickLoad.statuses != null) {
if (localQuickLoad != null && localQuickLoad.statuses != null && newStatus != null) {
for (int i = 0; i < localQuickLoad.statuses.size(); i++) {
if (localQuickLoad.statuses.get(i).id.equals(newStatus.id)) {
localQuickLoad.statuses.set(i, newStatus);
@ -345,7 +345,7 @@ public class QuickLoad {
e.printStackTrace();
}
}
if (publicQuickLoad != null && publicQuickLoad.statuses != null) {
if (publicQuickLoad != null && publicQuickLoad.statuses != null && newStatus != null) {
for (int i = 0; i < publicQuickLoad.statuses.size(); i++) {
if (publicQuickLoad.statuses.get(i).id.equals(newStatus.id)) {
publicQuickLoad.statuses.set(i, newStatus);

@ -370,6 +370,10 @@ public class Timeline {
LIST("LIST"),
@SerializedName("REMOTE")
REMOTE("REMOTE"),
@SerializedName("TREND_TAG")
TREND_TAG("TREND_TAG"),
@SerializedName("TREND_MESSAGE")
TREND_MESSAGE("TREND_MESSAGE"),
@SerializedName("ACCOUNT_TIMELINE")
ACCOUNT_TIMELINE("ACCOUNT_TIMELINE"),
@SerializedName("MUTED_TIMELINE")

@ -0,0 +1,249 @@
package app.fedilab.android.helper;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.content.res.ResourcesCompat;
import java.util.HashSet;
import java.util.Random;
import app.fedilab.android.R;
//Original work at https://stackoverflow.com/a/17830245
public class CirclesDrawingView extends View {
// Radius limit in pixels
private final static int RADIUS_LIMIT = 100;
private static final int CIRCLES_LIMIT = 1;
private final Random mRadiusGenerator = new Random();
/**
* All available circles
*/
private final HashSet<CircleArea> mCircles = new HashSet<>(CIRCLES_LIMIT);
private final SparseArray<CircleArea> mCirclePointer = new SparseArray<>(CIRCLES_LIMIT);
/**
* Paint to draw circles
*/
private Paint mCirclePaint;
private CircleArea touchedCircle;
/**
* Default constructor
*
* @param ct {@link android.content.Context}
*/
public CirclesDrawingView(final Context ct) {
super(ct);
init(ct);
}
public CirclesDrawingView(final Context ct, final AttributeSet attrs) {
super(ct, attrs);
init(ct);
}
public CirclesDrawingView(final Context ct, final AttributeSet attrs, final int defStyle) {
super(ct, attrs, defStyle);
init(ct);
}
public CircleArea getTouchedCircle() {
return this.touchedCircle;
}
private void init(final Context ct) {
// Generate bitmap used for background
mCirclePaint = new Paint();
mCirclePaint.setColor(ResourcesCompat.getColor(getContext().getResources(), R.color.cyanea_accent, getContext().getTheme()));
mCirclePaint.setStrokeWidth(10);
mCirclePaint.setStyle(Paint.Style.STROKE);
}
@Override
public void onDraw(final Canvas canv) {
// background bitmap to cover all area
for (CircleArea circle : mCircles) {
canv.drawCircle(circle.centerX, circle.centerY, circle.radius, mCirclePaint);
}
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
boolean handled = false;
int xTouch;
int yTouch;
int pointerId;
int actionIndex = event.getActionIndex();
// get touch event coordinates and make transparent circle from it
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// it's the first pointer, so clear all existing pointers data
clearCirclePointer();
xTouch = (int) event.getX(0);
yTouch = (int) event.getY(0);
// check if we've touched inside some circle
touchedCircle = obtainTouchedCircle(xTouch, yTouch);
touchedCircle.centerX = xTouch;
touchedCircle.centerY = yTouch;
mCirclePointer.put(event.getPointerId(0), touchedCircle);
invalidate();
handled = true;
break;
case MotionEvent.ACTION_POINTER_DOWN:
// It secondary pointers, so obtain their ids and check circles
pointerId = event.getPointerId(actionIndex);
xTouch = (int) event.getX(actionIndex);
yTouch = (int) event.getY(actionIndex);
// check if we've touched inside some circle
touchedCircle = obtainTouchedCircle(xTouch, yTouch);
mCirclePointer.put(pointerId, touchedCircle);
touchedCircle.centerX = xTouch;
touchedCircle.centerY = yTouch;
invalidate();
handled = true;
break;
case MotionEvent.ACTION_MOVE:
final int pointerCount = event.getPointerCount();
for (actionIndex = 0; actionIndex < pointerCount; actionIndex++) {
// Some pointer has moved, search it by pointer id
pointerId = event.getPointerId(actionIndex);
xTouch = (int) event.getX(actionIndex);
yTouch = (int) event.getY(actionIndex);
touchedCircle = mCirclePointer.get(pointerId);
if (null != touchedCircle) {
touchedCircle.centerX = xTouch;
touchedCircle.centerY = yTouch;
}
}
invalidate();
handled = true;
break;
case MotionEvent.ACTION_UP:
clearCirclePointer();
invalidate();
handled = true;
break;
case MotionEvent.ACTION_POINTER_UP:
// not general pointer was up
pointerId = event.getPointerId(actionIndex);
mCirclePointer.remove(pointerId);
invalidate();
handled = true;
break;
case MotionEvent.ACTION_CANCEL:
handled = true;
break;
default:
// do nothing
break;
}
return super.onTouchEvent(event) || handled;
}
/**
* Clears all CircleArea - pointer id relations
*/
private void clearCirclePointer() {
mCirclePointer.clear();
}
/**
* Search and creates new (if needed) circle based on touch area
*
* @param xTouch int x of touch
* @param yTouch int y of touch
* @return obtained {@link CircleArea}
*/
private CircleArea obtainTouchedCircle(final int xTouch, final int yTouch) {
CircleArea touchedCircle = getTouchedCircle(xTouch, yTouch);
if (null == touchedCircle) {
touchedCircle = new CircleArea(xTouch, yTouch, mRadiusGenerator.nextInt(RADIUS_LIMIT) + RADIUS_LIMIT);
if (mCircles.size() == CIRCLES_LIMIT) {
// remove first circle
mCircles.clear();
}
mCircles.add(touchedCircle);
}
return touchedCircle;
}
/**
* Determines touched circle
*
* @param xTouch int x touch coordinate
* @param yTouch int y touch coordinate
* @return {@link CircleArea} touched circle or null if no circle has been touched
*/
private CircleArea getTouchedCircle(final int xTouch, final int yTouch) {
CircleArea touched = null;
for (CircleArea circle : mCircles) {
if ((circle.centerX - xTouch) * (circle.centerX - xTouch) + (circle.centerY - yTouch) * (circle.centerY - yTouch) <= circle.radius * circle.radius) {
touched = circle;
break;
}
}
return touched;
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
/**
* Stores data about single circle
*/
public static class CircleArea {
public int centerX;
public int centerY;
int radius;
CircleArea(int centerX, int centerY, int radius) {
this.radius = radius;
this.centerX = centerX;
this.centerY = centerY;
}
@Override
public String toString() {
return "Circle[" + centerX + ", " + centerY + ", " + radius + "]";
}
}
}

@ -0,0 +1,209 @@
package app.fedilab.android.helper;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static com.bumptech.glide.load.resource.bitmap.TransformationUtils.PAINT_FLAGS;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.os.Build;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
import com.bumptech.glide.util.Synthetic;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import jp.wasabeef.glide.transformations.BitmapTransformation;
public class GlideFocus extends BitmapTransformation {
private static final int VERSION = 1;
private static final String ID = "app.fedilab.android.GlideFocus." + VERSION;
private static final Set<String> MODELS_REQUIRING_BITMAP_LOCK =
new HashSet<>(
Arrays.asList(
// Moto X gen 2
"XT1085",
"XT1092",
"XT1093",
"XT1094",
"XT1095",
"XT1096",
"XT1097",
"XT1098",
// Moto G gen 1
"XT1031",
"XT1028",
"XT937C",
"XT1032",
"XT1008",
"XT1033",
"XT1035",
"XT1034",
"XT939G",
"XT1039",
"XT1040",
"XT1042",
"XT1045",
// Moto G gen 2
"XT1063",
"XT1064",
"XT1068",
"XT1069",
"XT1072",
"XT1077",
"XT1078",
"XT1079"));
private static final Lock BITMAP_DRAWABLE_LOCK =
MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL) ? new ReentrantLock() : new NoLock();
private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS);
private final float focalX;
private final float focalY;
public GlideFocus(float focalX, float focalY) {
this.focalX = focalX;
this.focalY = focalY;
}
private static void applyMatrix(
@NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap, Matrix matrix) {
BITMAP_DRAWABLE_LOCK.lock();
try {
Canvas canvas = new Canvas(targetBitmap);
canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
clear(canvas);
} finally {
BITMAP_DRAWABLE_LOCK.unlock();
}
}
@NonNull
private static Bitmap.Config getNonNullConfig(@NonNull Bitmap bitmap) {
return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888;
}
// Avoids warnings in M+.
private static void clear(Canvas canvas) {
canvas.setBitmap(null);
}
@Override
protected Bitmap transform(@NonNull Context context, @NonNull BitmapPool pool,
@NonNull Bitmap inBitmap, int width, int height) {
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
return inBitmap;
}
// From ImageView/Bitmap.createScaledBitmap.
final float scale;
final float dx;
final float dy;
Matrix m = new Matrix();
if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) {
scale = (float) height / (float) inBitmap.getHeight();
dx = (width - inBitmap.getWidth() * scale) * 0.5f * (1 + focalX);
dy = 0;
} else {
scale = (float) width / (float) inBitmap.getWidth();
dx = 0;
dy = (height - inBitmap.getHeight() * scale) * 0.5f * (1 + focalY);
}
m.setScale(scale, scale);
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap));
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(inBitmap, result);
applyMatrix(inBitmap, result, m);
return result;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update((ID + focalX + focalY).getBytes(CHARSET));
}
@Override
public boolean equals(Object o) {
return o instanceof GlideFocus &&
((GlideFocus) o).focalX == focalX &&
((GlideFocus) o).focalY == focalY;
}
@Override
public int hashCode() {
return (int) (ID.hashCode() + focalX * 100000 + focalY * 1000);
}
@Override
public String toString() {
return "CropTransformation(width=" + focalX + ", height=" + focalY + ")";
}
private static final class NoLock implements Lock {
@Synthetic
NoLock() {
}
@Override
public void lock() {
// do nothing
}
@Override
public void lockInterruptibly() throws InterruptedException {
// do nothing
}
@Override
public boolean tryLock() {
return true;
}
@Override
public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException {
return true;
}
@Override
public void unlock() {
// do nothing
}
@NonNull
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Should not be called");
}
}
}

@ -1379,6 +1379,9 @@ public class Helper {
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
private static int notificationId = 1;
/**
* Sends notification with intent
*
@ -1389,7 +1392,7 @@ public class Helper {
* @param message String message for the notification
*/
@SuppressLint("UnspecifiedImmutableFlag")
public static void notify_user(Context context, int notificationId, BaseAccount account, Intent intent, Bitmap icon, NotifType notifType, String title, String message) {
public static void notify_user(Context context, BaseAccount account, Intent intent, Bitmap icon, NotifType notifType, String title, String message) {
final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
// prepare intent which is triggered if the user click on the notification
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
@ -1440,14 +1443,11 @@ public class Helper {
channelTitle = context.getString(R.string.channel_notif_boost);
}
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification).setTicker(message)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true);
.setSmallIcon(R.drawable.ic_notification).setTicker(message);
if (notifType == NotifType.MENTION) {
if (message.length() > 500) {
message = message.substring(0, 499) + "…";
}
notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message));
}
notificationBuilder.setGroup(account.mastodon_account.acct + "@" + account.instance)
.setContentIntent(pIntent)
@ -1515,19 +1515,21 @@ public class Helper {
}
notificationBuilder.setContentTitle(title);
notificationBuilder.setLargeIcon(icon);
notificationManager.notify(notificationId, notificationBuilder.build());
Notification summaryNotification =
new NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(channelTitle)
.setContentIntent(pIntent)
.setLargeIcon(icon)
.setSmallIcon(R.drawable.ic_notification)
.setGroup(account.mastodon_account.acct + "@" + account.instance)
.setGroupSummary(true)
.build();
notificationManager.notify(notificationId, summaryNotification);
Notification summaryNotification = summaryNotification = new NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(channelTitle)
.setContentIntent(pIntent)
.setLargeIcon(icon)
.setSmallIcon(R.drawable.ic_notification)
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
.setGroup(account.mastodon_account.acct + "@" + account.instance)
.setGroupSummary(true)
.build();
notificationManager.notify(notificationId++, notificationBuilder.build());
notificationManager.notify(0, summaryNotification);
}
public static void transfertIfExist(Context context) {

@ -163,7 +163,7 @@ public class MediaHelper {
Uri uri = Uri.fromFile(backupFile);
intent.setDataAndType(uri, mime);
if (!share) {
notify_user(context, Helper.NOTIFICATION_MEDIA, currentAccount, intent, BitmapFactory.decodeResource(context.getResources(),
notify_user(context, currentAccount, intent, BitmapFactory.decodeResource(context.getResources(),
R.mipmap.ic_launcher), Helper.NotifType.STORE, context.getString(R.string.save_over), context.getString(R.string.download_from, fileName));
Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show();
} else {

@ -1,113 +0,0 @@
package app.fedilab.android.helper
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

@ -317,7 +317,7 @@ public class NotificationsHelper {
since_ids.put(account.user_id + "@" + account.instance, lastNotif);
editor.putString(context.getString(R.string.LAST_NOTIFICATION_ID) + account.user_id + "@" + account.instance, notifications.get(0).id);
editor.apply();
notify_user(context, Helper.NOTIFICATION_USER_NOTIF, account, intent, BitmapFactory.decodeResource(context.getResources(),
notify_user(context, account, intent, BitmapFactory.decodeResource(context.getResources(),
R.mipmap.ic_launcher), finalNotifType, finalTitle, finalMessage);
}
return false;
@ -332,7 +332,7 @@ public class NotificationsHelper {
editor.putString(context.getString(R.string.LAST_NOTIFICATION_ID) + account.user_id + "@" + account.instance, notifications.get(0).id);
editor.apply();
since_ids.put(account.user_id + "@" + account.instance, lastNotif);
notify_user(context, Helper.NOTIFICATION_USER_NOTIF, account, intent, resource, finalNotifType, finalTitle, finalMessage);
notify_user(context, account, intent, resource, finalNotifType, finalTitle, finalMessage);
}
}

@ -26,16 +26,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager2.widget.ViewPager2;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
import java.util.Arrays;
@ -149,18 +147,11 @@ public class PinnedTimelineHelper {
activityMainBinding.tabLayout.removeAllTabs();
//Small hack to hide first tabs (they represent the item of the bottom menu)
int toRemove = itemToRemoveInBottomMenu(activity);
List<String> tabTitle = new ArrayList<>();
List<RemoteInstance.InstanceType> tabTypeRemote = new ArrayList<>();
List<Timeline.TimeLineEnum> tabType = new ArrayList<>();
for (int i = 0; i < (BOTTOM_TIMELINE_COUNT - toRemove); i++) {
activityMainBinding.tabLayout.addTab(activityMainBinding.tabLayout.newTab());
tabTitle.add("");
tabType.add(Timeline.TimeLineEnum.HOME);
tabTypeRemote.add(RemoteInstance.InstanceType.MASTODON);
((ViewGroup) activityMainBinding.tabLayout.getChildAt(0)).getChildAt(i).setVisibility(View.GONE);
}
List<PinnedTimeline> pinnedTimelineVisibleList = new ArrayList<>();
for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) {
if (pinnedTimeline.displayed) {
TabLayout.Tab tab = activityMainBinding.tabLayout.newTab();
@ -170,22 +161,46 @@ public class PinnedTimelineHelper {
name = pinnedTimeline.mastodonList.title;
break;
case TAG:
name = pinnedTimeline.tagTimeline.name;
name = pinnedTimeline.tagTimeline.name.replaceAll("#", "");
break;
case REMOTE:
name = pinnedTimeline.remoteInstance.host;
break;
}
TextView tv = (TextView) LayoutInflater.from(activity).inflate(R.layout.custom_tab_instance, new LinearLayout(activity), false);
tv.setText(name);
tabTitle.add(name);
tabType.add(pinnedTimeline.type);
if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) {
tabTypeRemote.add(pinnedTimeline.remoteInstance.type);
} else {
tabTypeRemote.add(null);
TabCustomViewBinding tabCustomViewBinding = TabCustomViewBinding.inflate(activity.getLayoutInflater());
tabCustomViewBinding.title.setText(name);
switch (pinnedTimeline.type) {
case LIST:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_list);
break;
case TAG:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_tag);
break;
case REMOTE:
switch (pinnedTimeline.remoteInstance.type) {
case PIXELFED:
tabCustomViewBinding.icon.setImageResource(R.drawable.pixelfed);
break;
case MASTODON:
tabCustomViewBinding.icon.setImageResource(R.drawable.mastodon_icon_item);
break;
case MISSKEY:
tabCustomViewBinding.icon.setImageResource(R.drawable.misskey);
break;
case NITTER:
tabCustomViewBinding.icon.setImageResource(R.drawable.nitter);
break;
case GNU:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_gnu_social);
break;
case PEERTUBE:
tabCustomViewBinding.icon.setImageResource(R.drawable.peertube_icon);
break;
}
break;
}
tab.setCustomView(tv);
tab.setCustomView(tabCustomViewBinding.getRoot());
activityMainBinding.tabLayout.addTab(tab);
pinnedTimelineVisibleList.add(pinnedTimeline);
}
@ -211,13 +226,18 @@ public class PinnedTimelineHelper {
return true;
});
}
activityMainBinding.viewPager.setAdapter(null);
activityMainBinding.viewPager.clearOnPageChangeListeners();
activityMainBinding.tabLayout.clearOnTabSelectedListeners();
FedilabPageAdapter fedilabPageAdapter = new FedilabPageAdapter(activity, activity, pinned, bottomMenu);
FedilabPageAdapter fedilabPageAdapter = new FedilabPageAdapter(activity, activity.getSupportFragmentManager(), pinned, bottomMenu);
activityMainBinding.viewPager.setAdapter(fedilabPageAdapter);
activityMainBinding.viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(activityMainBinding.tabLayout));
activityMainBinding.viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
activityMainBinding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
if (position < BOTTOM_TIMELINE_COUNT - toRemove) {
@ -230,49 +250,17 @@ public class PinnedTimelineHelper {
activityMainBinding.bottomNavView.getMenu().setGroupCheckable(0, true, true);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
new TabLayoutMediator(activityMainBinding.tabLayout, activityMainBinding.viewPager,
(tab, position) -> {
TabCustomViewBinding tabCustomViewBinding = TabCustomViewBinding.inflate(activity.getLayoutInflater());
tabCustomViewBinding.title.setText(tabTitle.get(position));
switch (tabType.get(position)) {
case LIST:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_list);
break;
case TAG:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_tag);
break;
case REMOTE:
switch (tabTypeRemote.get(position)) {
case PIXELFED:
tabCustomViewBinding.icon.setImageResource(R.drawable.pixelfed);
break;
case MASTODON:
tabCustomViewBinding.icon.setImageResource(R.drawable.mastodon_icon_item);
break;
case MISSKEY:
tabCustomViewBinding.icon.setImageResource(R.drawable.misskey);
break;
case NITTER:
tabCustomViewBinding.icon.setImageResource(R.drawable.nitter);
break;
case GNU:
tabCustomViewBinding.icon.setImageResource(R.drawable.ic_gnu_social);
break;
case PEERTUBE:
tabCustomViewBinding.icon.setImageResource(R.drawable.peertube_icon);
break;
}
break;
}
tab.setCustomView(tabCustomViewBinding.getRoot());
}
).attach();
activityMainBinding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
activityMainBinding.viewPager.setCurrentItem(tab.getPosition());
}
@Override
@ -281,7 +269,7 @@ public class PinnedTimelineHelper {
@Override
public void onTabReselected(TabLayout.Tab tab) {
Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem());
Fragment fragment = fedilabPageAdapter.getCurrentFragment();
if (fragment instanceof FragmentMastodonTimeline) {
((FragmentMastodonTimeline) fragment).scrollToTop();
} else if (fragment instanceof FragmentMastodonConversation) {
@ -353,19 +341,19 @@ public class PinnedTimelineHelper {
itemShowNSFW.setChecked(showNSFW[0]);
popup.setOnDismissListener(menu1 -> {
if (changes[0]) {
FragmentMastodonTimeline fragmentMastodonTimeline;
Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction();
fragTransaction.detach(fragmentMastodonTimeline).commit();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG);
bundle.putSerializable(Helper.ARG_TAG_TIMELINE, tagTimeline);
fragmentMastodonTimeline.setArguments(bundle);
FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction();
fragTransaction2.attach(fragmentMastodonTimeline);
fragTransaction2.commit();
if (activityMainBinding.viewPager.getAdapter() != null) {
Fragment fragmentMastodonTimeline = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
if (fragmentMastodonTimeline instanceof FragmentMastodonTimeline && fragmentMastodonTimeline.isVisible()) {
FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction();
fragTransaction.detach(fragmentMastodonTimeline).commit();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG);
bundle.putSerializable(Helper.ARG_TAG_TIMELINE, tagTimeline);
fragmentMastodonTimeline.setArguments(bundle);
FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction();
fragTransaction2.attach(fragmentMastodonTimeline);
fragTransaction2.commit();
}
}
}
});
@ -568,9 +556,11 @@ public class PinnedTimelineHelper {
});
changes[0] = true;
FragmentMastodonTimeline fragmentMastodonTimeline = null;
Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
if (activityMainBinding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
}
}
if (fragmentMastodonTimeline == null)
return false;
@ -613,10 +603,12 @@ public class PinnedTimelineHelper {
MenuItem item = popup.getMenu().add(0, 0, Menu.NONE, title);
item.setOnMenuItemClickListener(item1 -> {
FragmentMastodonTimeline fragmentMastodonTimeline = null;
Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
if (activityMainBinding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
}
}
FragmentTransaction fragTransaction1 = activity.getSupportFragmentManager().beginTransaction();
if (fragmentMastodonTimeline == null)
@ -694,10 +686,12 @@ public class PinnedTimelineHelper {
popup.setOnDismissListener(menu -> {
if (changes[0]) {
FragmentMastodonTimeline fragmentMastodonTimeline = null;
Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
if (activityMainBinding.viewPager.getAdapter() != null) {
Fragment fragment = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.refreshAllAdapters();
}
}
FragmentTransaction fragTransaction1 = activity.getSupportFragmentManager().beginTransaction();
if (fragmentMastodonTimeline == null)

@ -256,14 +256,15 @@ public class SpannableHelper {
content.setSpan(new LongClickableSpan() {
@Override
public void onLongClick(View view) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle());
Context mContext = view.getContext();
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext, Helper.dialogStyle());
PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context));
dialogBuilder.setView(popupLinksBinding.getRoot());
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();
popupLinksBinding.displayFullLink.setOnClickListener(v -> {
AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle());
builder.setMessage(url);
builder.setTitle(context.getString(R.string.display_full_link));
builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
@ -720,14 +721,15 @@ public class SpannableHelper {
content.setSpan(new LongClickableSpan() {
@Override
public void onLongClick(View view) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
Context mContext = view.getContext();
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext, Helper.dialogStyle());
PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context));
dialogBuilder.setView(popupLinksBinding.getRoot());
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();
popupLinksBinding.displayFullLink.setOnClickListener(v -> {
AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle());
builder.setMessage(url);
builder.setTitle(context.getString(R.string.display_full_link));
builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())

@ -5,6 +5,7 @@ import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
@ -33,6 +34,7 @@ import java.io.InputStream;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityEditImageBinding;
import app.fedilab.android.helper.CirclesDrawingView;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.imageeditor.base.BaseActivity;
import app.fedilab.android.imageeditor.filters.FilterListener;
@ -58,7 +60,6 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
private static final int CAMERA_REQUEST = 52;
private static final int PICK_REQUEST = 53;
private final int STORE_REQUEST = 54;
private final EditingToolsAdapter mEditingToolsAdapter = new EditingToolsAdapter(this);
private final FilterViewAdapter mFilterViewAdapter = new FilterViewAdapter(this);
private final ConstraintSet mConstraintSet = new ConstraintSet();
@ -117,8 +118,6 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
binding.rvFilterView.setLayoutManager(llmFilters);
binding.rvFilterView.setAdapter(mFilterViewAdapter);
//Typeface mTextRobotoTf = ResourcesCompat.getFont(this, R.font.roboto_medium);
//Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf");
Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf");
mPhotoEditor = new PhotoEditor.Builder(this, binding.photoEditorView)
@ -246,6 +245,49 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
if (exit) {
Intent intentImage = new Intent(Helper.INTENT_SEND_MODIFIED_IMAGE);
intentImage.putExtra("imgpath", imagePath);
CirclesDrawingView.CircleArea circleArea = binding.focusCircle.getTouchedCircle();
if (circleArea != null) {
//Dimension of the editor containing the image
int pHeight = binding.photoEditorView.getHeight();
int pWidth = binding.photoEditorView.getWidth();
//Load the original image in a bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(new File(imagePath).getAbsolutePath(), options);
//Get height and width of the original image
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
//Evaluate the dimension of the image in the editor
int imgHeightInEditor;
int imgWidthInEditor;
//If the original image has its height greater than width => heights are equals
float focusX = -2, focusY = -2;
if (imageHeight > imageWidth) {
imgHeightInEditor = pHeight;
float ratio = (float) pHeight / (float) imageHeight;
imgWidthInEditor = (int) (pWidth * ratio);
} else { //Otherwise widths are equals
imgWidthInEditor = pWidth;
float ratio = (float) pWidth / (float) imageWidth;
imgHeightInEditor = (int) (pHeight * ratio);
}
focusY = (float) (circleArea.centerY * 2 - imgHeightInEditor / 2) / (float) imgHeightInEditor - 0.5f;
focusX = (float) (circleArea.centerX * 2 - imgWidthInEditor / 2) / (float) imgWidthInEditor - 0.5f;
if (focusX > 1) {
focusX = 1;
} else if (focusX < -1) {
focusX = -1;
}
if (focusY > 1) {
focusY = 1;
} else if (focusY < -1) {
focusY = -1;
}
intentImage.putExtra("focusX", focusX);
intentImage.putExtra("focusY", focusY);
}
LocalBroadcastManager.getInstance(EditImageActivity.this).sendBroadcast(intentImage);
finish();
}
@ -376,6 +418,7 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
@Override
public void onToolSelected(ToolType toolType) {
binding.focusCircle.setVisibility(View.GONE);
switch (toolType) {
case SHAPE:
mPhotoEditor.setBrushDrawingMode(true);
@ -414,6 +457,9 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
CropImage.activity(uri)
.start(this);
break;
case FOCUS:
binding.focusCircle.setVisibility(View.VISIBLE);
break;
}
}

@ -32,6 +32,7 @@ public class EditingToolsAdapter extends RecyclerView.Adapter<EditingToolsAdapte
mToolList.add(new ToolModel("Eraser", R.drawable.ic_eraser, ToolType.ERASER));
mToolList.add(new ToolModel("Filter", R.drawable.ic_photo_filter, ToolType.FILTER));
mToolList.add(new ToolModel("Emoji", R.drawable.ic_insert_emoticon, ToolType.EMOJI));
mToolList.add(new ToolModel("Focus", R.drawable.ic_baseline_filter_center_focus_24, ToolType.FOCUS));
}
@NonNull

@ -13,5 +13,6 @@ public enum ToolType {
FILTER,
EMOJI,
STICKER,
CROP
CROP,
FOCUS
}

@ -294,7 +294,8 @@ public class PostMessageService extends IntentService {
}
private static String postAttachment(MastodonStatusesService mastodonStatusesService, DataPost dataPost, MultipartBody.Part fileMultipartBody, Attachment attachment) {
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, attachment.description, null);
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, attachment.description, attachment.focus);
if (attachmentCall != null) {
try {

@ -285,25 +285,25 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
String[] mimetypes = new String[0];
if (type == ComposeActivity.mediaType.PHOTO) {
if (instanceInfo.getMimeTypeImage() != null && instanceInfo.getMimeTypeImage().size() > 0) {
if (instanceInfo != null && instanceInfo.getMimeTypeImage() != null && instanceInfo.getMimeTypeImage().size() > 0) {
mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]);
} else {
mimetypes = new String[]{"image/*"};
}
} else if (type == ComposeActivity.mediaType.VIDEO) {
if (instanceInfo.getMimeTypeVideo() != null && instanceInfo.getMimeTypeVideo().size() > 0) {
if (instanceInfo != null && instanceInfo.getMimeTypeVideo() != null && instanceInfo.getMimeTypeVideo().size() > 0) {
mimetypes = instanceInfo.getMimeTypeVideo().toArray(new String[0]);
} else {
mimetypes = new String[]{"video/*"};
}
} else if (type == ComposeActivity.mediaType.AUDIO) {
if (instanceInfo.getMimeTypeAudio() != null && instanceInfo.getMimeTypeAudio().size() > 0) {
if (instanceInfo != null && instanceInfo.getMimeTypeAudio() != null && instanceInfo.getMimeTypeAudio().size() > 0) {
mimetypes = instanceInfo.getMimeTypeAudio().toArray(new String[0]);
} else {
mimetypes = new String[]{"audio/mpeg", "audio/opus", "audio/flac", "audio/wav", "audio/ogg"};
}
} else if (type == ComposeActivity.mediaType.ALL) {
if (instanceInfo.getMimeTypeOther() != null && instanceInfo.getMimeTypeOther().size() > 0) {
if (instanceInfo != null && instanceInfo.getMimeTypeOther() != null && instanceInfo.getMimeTypeOther().size() > 0) {
mimetypes = instanceInfo.getMimeTypeOther().toArray(new String[0]);
} else {
mimetypes = new String[]{"*/*"};
@ -706,7 +706,12 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
public void afterTextChanged(Editable s) {
int currentLength = MastodonHelper.countLength(holder);
//Copy/past
int max_car = instanceInfo.max_toot_chars != null ? Integer.parseInt(instanceInfo.max_toot_chars) : instanceInfo.configuration.statusesConf.max_characters;
int max_car;
if (instanceInfo != null) {
max_car = instanceInfo.max_toot_chars != null ? Integer.parseInt(instanceInfo.max_toot_chars) : instanceInfo.configuration.statusesConf.max_characters;
} else {
max_car = 500;
}
if (currentLength > max_car + 1) {
int from = max_car - holder.binding.contentSpoiler.getText().length();
int to = (currentLength - holder.binding.contentSpoiler.getText().length());
@ -1073,7 +1078,8 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
});
//Disable buttons to attach media if max has been reached
if (statusDraft.media_attachments != null && statusDraft.media_attachments.size() >= instanceInfo.configuration.statusesConf.max_media_attachments) {
if (statusDraft.media_attachments != null &&
((instanceInfo != null && statusDraft.media_attachments.size() >= instanceInfo.configuration.statusesConf.max_media_attachments) || (instanceInfo == null && statusDraft.media_attachments.size() >= 4))) {
holder.binding.buttonAttachImage.setEnabled(false);
holder.binding.buttonAttachVideo.setEnabled(false);
holder.binding.buttonAttachAudio.setEnabled(false);

@ -0,0 +1,85 @@
package app.fedilab.android.ui.drawer;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Context;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Field;
import app.fedilab.android.databinding.DrawerFieldBinding;
public class FieldAdapter extends RecyclerView.Adapter<FieldAdapter.FieldViewHolder> {
private final List<Field> fields;
private Context context;
public FieldAdapter(List<Field> fields) {
this.fields = fields;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return fields.size();
}
@NonNull
@Override
public FieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
context = parent.getContext();
DrawerFieldBinding itemBinding = DrawerFieldBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new FieldViewHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull FieldViewHolder holder, int position) {
Field field = fields.get(position);
if (field.verified_at != null) {
holder.binding.value.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(context, R.drawable.ic_baseline_verified_24), null);
field.value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.verified_text)), 0, field.value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
holder.binding.value.setText(field.value_span != null ? field.value_span : field.value, TextView.BufferType.SPANNABLE);
holder.binding.value.setMovementMethod(LinkMovementMethod.getInstance());
holder.binding.label.setText(field.name);
}
public static class FieldViewHolder extends RecyclerView.ViewHolder {
DrawerFieldBinding binding;
FieldViewHolder(DrawerFieldBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
}
}

@ -143,13 +143,21 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
});
} else if (viewHolder.getItemViewType() == NOTIFICATION_FETCH_MORE) {
StatusAdapter.StatusViewHolder holder = (StatusAdapter.StatusViewHolder) viewHolder;
holder.bindingFetchMore.fetchMore.setEnabled(!notification.isFetchMoreHidden);
holder.bindingFetchMore.fetchMore.setOnClickListener(v -> {
holder.bindingFetchMore.fetchMoreContainer.setEnabled(!notification.isFetchMoreHidden);
holder.bindingFetchMore.fetchMoreMin.setOnClickListener(v -> {
if (position + 1 < notificationList.size()) {
//We hide the button
notification.isFetchMoreHidden = true;
notifyItemChanged(position);
fetchMoreCallBack.onClick(notificationList.get(position + 1).id, notification.id);
fetchMoreCallBack.onClickMin(notificationList.get(position + 1).id, notification.id);
}
});
holder.bindingFetchMore.fetchMoreMax.setOnClickListener(v -> {
if (position - 1 >= 0) {
//We hide the button
notification.isFetchMoreHidden = true;
notifyItemChanged(position);
fetchMoreCallBack.onClickMax(notificationList.get(position - 1).id, notification.id);
}
});
} else {
@ -264,7 +272,9 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
}
public interface FetchMoreCallBack {
void onClick(String min_id, String fetchmoreId);
void onClickMin(String min_id, String fetchmoreId);
void onClickMax(String max_id, String fetchmoreId);
}
static class ViewHolderFollow extends RecyclerView.ViewHolder {

@ -117,6 +117,7 @@ import app.fedilab.android.databinding.LayoutMediaBinding;
import app.fedilab.android.databinding.LayoutPollItemBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.CrossActionHelper;
import app.fedilab.android.helper.GlideFocus;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.LongClickLinkMovementMethod;
import app.fedilab.android.helper.MastodonHelper;
@ -984,10 +985,18 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
} else {
layoutMediaBinding.playMusic.setVisibility(View.GONE);
}
float focusX = 0.f;
float focusY = 0.f;
if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) {
focusX = statusToDeal.media_attachments.get(0).meta.focus.x;
focusY = statusToDeal.media_attachments.get(0).meta.focus.y;
}
if (!mediaObfuscated(statusToDeal) || expand_media) {
layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24);
Glide.with(layoutMediaBinding.media.getContext())
.load(statusToDeal.media_attachments.get(0).preview_url)
.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY)))
.into(layoutMediaBinding.media);
} else {
layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24);
@ -995,6 +1004,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
.load(statusToDeal.media_attachments.get(0).preview_url)
.apply(new RequestOptions().transform(new BlurTransformation(50, 3)))
// .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context))))
.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY)))
.into(layoutMediaBinding.media);
}
layoutMediaBinding.viewHide.setOnClickListener(v -> {
@ -1009,6 +1019,13 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
for (Attachment attachment : statusToDeal.media_attachments) {
LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false);
RelativeLayout.LayoutParams lp;
float focusX = 0.f;
float focusY = 0.f;
if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) {
focusX = statusToDeal.media_attachments.get(0).meta.focus.x;
focusY = statusToDeal.media_attachments.get(0).meta.focus.y;
}
if (fullAttachement) {
lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER);
@ -1032,12 +1049,14 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
Glide.with(layoutMediaBinding.media.getContext())
.load(attachment.preview_url)
.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context))))
.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY)))
.into(layoutMediaBinding.media);
} else {
layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24);
Glide.with(layoutMediaBinding.media.getContext())
.load(attachment.preview_url)
.apply(new RequestOptions().transform(new BlurTransformation(50, 3)))
.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY)))
// .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context))))
.into(layoutMediaBinding.media);
}
@ -1172,6 +1191,9 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
j++;
}
} else {
if (statusToDeal.poll.voters_count == 0 && statusToDeal.poll.votes_count > 0) {
statusToDeal.poll.voters_count = statusToDeal.poll.votes_count;
}
holder.binding.poll.rated.setVisibility(View.GONE);
holder.binding.poll.submitVote.setVisibility(View.VISIBLE);
if (statusToDeal.poll.multiple) {
@ -1828,13 +1850,21 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
});
} else if (viewHolder.getItemViewType() == STATUS_FETCH_MORE) {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.bindingFetchMore.fetchMore.setEnabled(!status.isFetchMoreHidden);
holder.bindingFetchMore.fetchMore.setOnClickListener(v -> {
holder.bindingFetchMore.fetchMoreContainer.setEnabled(!status.isFetchMoreHidden);
holder.bindingFetchMore.fetchMoreMin.setOnClickListener(v -> {
if (position + 1 < statusList.size()) {
//We hide the button
status.isFetchMoreHidden = true;
notifyItemChanged(position);
fetchMoreCallBack.onClick(statusList.get(position + 1).id, status.id);
fetchMoreCallBack.onClickMinId(statusList.get(position + 1).id, status.id);
}
});
holder.bindingFetchMore.fetchMoreMax.setOnClickListener(v -> {
if (position - 1 >= 0) {
//We hide the button
status.isFetchMoreHidden = true;
notifyItemChanged(position);
fetchMoreCallBack.onClickMaxId(statusList.get(position - 1).id, status.id);
}
});
}
@ -1857,7 +1887,9 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
public interface FetchMoreCallBack {
void onClick(String min_id, String fetchmoreId);
void onClickMinId(String min_id, String fetchmoreId);
void onClickMaxId(String max_id, String fetchmoreId);
}
public static class StatusViewHolder extends RecyclerView.ViewHolder {

@ -179,7 +179,13 @@ public class FragmentNotificationsSettings extends PreferenceFragmentCompat impl
PushHelper.startStreaming(requireActivity());
}
if (key.compareToIgnoreCase(getString(R.string.SET_LED_COLOUR_VAL)) == 0) {
sharedPreferences.edit().putInt(getString(R.string.SET_LED_COLOUR_VAL), Integer.parseInt(key)).apply();
try {
int value = Integer.parseInt(key);
sharedPreferences.edit().putInt(getString(R.string.SET_LED_COLOUR_VAL), value).apply();
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}

@ -571,7 +571,7 @@ public class FragmentThemingSettings extends PreferenceFragmentCompat implements
Uri uri = Uri.parse("file://" + fullPath);
intentOpen.setDataAndType(uri, "text/csv");
String title = getString(R.string.data_export_theme);
Helper.notify_user(getActivity(), Helper.NOTIFICATION_THEMING, currentAccount, intentOpen, BitmapFactory.decodeResource(requireActivity().getResources(),
Helper.notify_user(getActivity(), currentAccount, intentOpen, BitmapFactory.decodeResource(requireActivity().getResources(),
R.mipmap.ic_launcher), Helper.NotifType.BACKUP, title, message);
} catch (Exception e) {
e.printStackTrace();

@ -45,6 +45,7 @@ import app.fedilab.android.ui.drawer.AccountAdapter;
import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.SearchVM;
import es.dmoral.toasty.Toasty;
public class FragmentMastodonAccount extends Fragment {
@ -116,11 +117,15 @@ public class FragmentMastodonAccount extends Fragment {
SearchVM searchVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, SearchVM.class);
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL)
.observe(getViewLifecycleOwner(), results -> {
Accounts accounts = new Accounts();
Pagination pagination = new Pagination();
accounts.accounts = results.accounts;
accounts.pagination = pagination;
initializeAccountCommonView(accounts);
if (results != null) {
Accounts accounts = new Accounts();
Pagination pagination = new Pagination();
accounts.accounts = results.accounts;
accounts.pagination = pagination;
initializeAccountCommonView(accounts);
} else {
Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
}
});
} else if (timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE) {
if (firstLoad) {

@ -14,8 +14,6 @@ package app.fedilab.android.ui.fragment.timeline;
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -32,7 +30,6 @@ import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Announcement;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.AnnouncementAdapter;
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
@ -43,7 +40,6 @@ public class FragmentMastodonAnnouncement extends Fragment {
private FragmentPaginationBinding binding;
private AnnouncementsVM announcementsVM;
private AnnouncementAdapter announcementAdapter;
public View onCreateView(@NonNull LayoutInflater inflater,
@ -65,13 +61,6 @@ public class FragmentMastodonAnnouncement extends Fragment {
return root;
}
@Override
public void onResume() {
super.onResume();
NotificationManager notificationManager = (NotificationManager) requireActivity().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Helper.NOTIFICATION_USER_NOTIF);
}
/**
* Intialize the view for announcements
*
@ -92,7 +81,7 @@ public class FragmentMastodonAnnouncement extends Fragment {
}
announcementAdapter = new AnnouncementAdapter(announcements);
AnnouncementAdapter announcementAdapter = new AnnouncementAdapter(announcements);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(announcementAdapter);

@ -20,9 +20,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.service.notification.StatusBarNotification;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -86,7 +88,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati
}
}
};
private String max_id, min_id, min_id_fetch_more;
private String max_id, min_id, min_id_fetch_more, max_id_fetch_more;
private LinearLayoutManager mLayoutManager;
private String instance, user_id;
private ArrayList<String> idOfAddedNotifications;
@ -179,8 +181,16 @@ public class FragmentMastodonNotification extends Fragment implements Notificati
@Override
public void onResume() {
super.onResume();
NotificationManager notificationManager = (NotificationManager) requireActivity().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Helper.NOTIFICATION_USER_NOTIF);
NotificationManager mNotificationManager = (NotificationManager) requireActivity().getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BaseMainActivity.currentAccount != null && BaseMainActivity.currentAccount.mastodon_account != null) {
for (StatusBarNotification statusBarNotification : mNotificationManager.getActiveNotifications()) {
if ((BaseMainActivity.currentAccount.mastodon_account.acct + "@" + BaseMainActivity.currentAccount.instance).equals(statusBarNotification.getGroupKey())) {
mNotificationManager.cancel(statusBarNotification.getId());
}
}
} else {
mNotificationManager.cancelAll();
}
}
@ -306,8 +316,8 @@ public class FragmentMastodonNotification extends Fragment implements Notificati
notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
.observe(getViewLifecycleOwner(), this::initializeNotificationView);
} else if (direction == FragmentMastodonTimeline.DIRECTION.BOTTOM) {
notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
.observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, false));
notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
.observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == FragmentMastodonTimeline.DIRECTION.TOP) {
notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
.observe(getViewLifecycleOwner(), notificationsTop -> dealWithPagination(notificationsTop, FragmentMastodonTimeline.DIRECTION.TOP, fetchingMissing));
@ -500,7 +510,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati
}
@Override
public void onClick(String min_id, String id) {
public void onClickMin(String min_id, String id) {
//Fetch more has been pressed
min_id_fetch_more = min_id;
Notification notification = null;
@ -519,6 +529,25 @@ public class FragmentMastodonNotification extends Fragment implements Notificati
route(FragmentMastodonTimeline.DIRECTION.TOP, true);
}
@Override
public void onClickMax(String max_id, String id) {
//Fetch more has been pressed
max_id_fetch_more = max_id;
Notification notification = null;
int position = 0;
for (Notification currentNotification : this.notificationList) {
if (currentNotification.id.compareTo(id) == 0) {
notification = currentNotification;
break;
}
position++;
}
if (notification != null) {
this.notificationList.remove(position);
notificationAdapter.notifyItemRemoved(position);
}
route(FragmentMastodonTimeline.DIRECTION.BOTTOM, true);
}
public enum NotificationTypeEnum {
@SerializedName("ALL")

@ -31,12 +31,14 @@ import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Tag;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.TagAdapter;
import app.fedilab.android.viewmodel.mastodon.SearchVM;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
public class FragmentMastodonTag extends Fragment {
@ -45,11 +47,13 @@ public class FragmentMastodonTag extends Fragment {
private FragmentPaginationBinding binding;
private TagAdapter tagAdapter;
private String search;
private Timeline.TimeLineEnum timelineType;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
if (getArguments() != null) {
search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null);
timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE);
}
binding = FragmentPaginationBinding.inflate(inflater, container, false);
@ -74,7 +78,7 @@ public class FragmentMastodonTag extends Fragment {
* Router for timelines
*/
private void router() {
if (search != null) {
if (search != null && timelineType == null) {
SearchVM searchVM = new ViewModelProvider(FragmentMastodonTag.this).get(SearchVM.class);
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL)
.observe(getViewLifecycleOwner(), results -> {
@ -82,6 +86,12 @@ public class FragmentMastodonTag extends Fragment {
initializeTagCommonView(results.hashtags);
}
});
} else if (timelineType == Timeline.TimeLineEnum.TREND_TAG) {
TimelinesVM timelinesVM = new ViewModelProvider(FragmentMastodonTag.this).get(TimelinesVM.class);
timelinesVM.getTagsTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance)
.observe(getViewLifecycleOwner(), tags -> {
initializeTagCommonView(tags);
});
}
}

@ -77,7 +77,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
private List<Status> statuses;
private String search, searchCache;
private Status statusReport;
private String max_id, min_id, min_id_fetch_more;
private String max_id, min_id, min_id_fetch_more, max_id_fetch_more;
private StatusAdapter statusAdapter;
private Timeline.TimeLineEnum timelineType;
//Handle actions that can be done in other fragments
@ -286,11 +286,13 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
binding.loader.setVisibility(View.GONE);
binding.noAction.setVisibility(View.GONE);
binding.swipeContainer.setRefreshing(false);
binding.swipeContainer.setOnRefreshListener(() -> {
binding.swipeContainer.setRefreshing(true);
flagLoading = false;
route(DIRECTION.REFRESH, true);
});
if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) {
binding.swipeContainer.setOnRefreshListener(() -> {
binding.swipeContainer.setRefreshing(true);
flagLoading = false;
route(DIRECTION.REFRESH, true);
});
}
if (statuses == null || statuses.statuses == null || statuses.statuses.size() == 0) {
binding.noAction.setVisibility(View.VISIBLE);
@ -356,7 +358,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
binding.recyclerView.scrollToPosition(position);
}
if (searchCache == null) {
if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) {
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
@ -453,9 +455,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
insertedPosition = insertStatus(statusReceived);
if (insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM) {
numberInserted++;
//Find the first position of insertion, the initial id is set to STATUS_PRESENT
if (initialInsertedPosition == STATUS_PRESENT) {
initialInsertedPosition = insertedPosition;
}
//If next statuses have a lower id, there are inserted before (normally, that should not happen)
if (insertedPosition < initialInsertedPosition) {
initialInsertedPosition = lastInsertedPosition;
}
@ -464,12 +468,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
lastInsertedPosition = initialInsertedPosition + numberInserted;
//lastInsertedPosition contains the position of the last inserted status
//If there were no overlap for top status
if (fetchingMissing && insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM && this.statuses.size() > insertedPosition) {
if (fetchingMissing && insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM && this.statuses.size() > insertedPosition && numberInserted == MastodonHelper.statusesPerCall(requireActivity())) {
Status statusFetchMore = new Status();
statusFetchMore.isFetchMore = true;
statusFetchMore.id = Helper.generateString();
int insertAt;
if (direction == DIRECTION.REFRESH) {
if (direction == DIRECTION.REFRESH || direction == DIRECTION.BOTTOM) {
insertAt = lastInsertedPosition;
} else {
insertAt = initialInsertedPosition;
@ -485,7 +489,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
/**
* Insert a status if not yet in the timeline
* Insert a status if not yet in the timeline and returns its position of insertion
*
* @param statusReceived - Status coming from the api/db
* @return int >= 0 | STATUS_PRESENT = -1 | STATUS_AT_THE_BOTTOM = -2
@ -618,8 +622,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == DIRECTION.TOP) {
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing));
@ -638,8 +642,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == DIRECTION.TOP) {
timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing));
@ -724,8 +728,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getPublic(null, remoteInstance, true, false, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getPublic(null, remoteInstance, true, false, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == DIRECTION.TOP) {
timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing));
@ -745,8 +749,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == DIRECTION.TOP) {
timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing));
@ -769,8 +773,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing));
} else if (direction == DIRECTION.TOP) {
timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing));
@ -847,6 +851,19 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
} else {
flagLoading = false;
}
} else if (timelineType == Timeline.TimeLineEnum.TREND_MESSAGE) {
if (direction == null) {
timelinesVM.getStatusTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance)
.observe(getViewLifecycleOwner(), statusesTrends -> {
Statuses statuses = new Statuses();
statuses.statuses = new ArrayList<>();
if (statusesTrends != null) {
statuses.statuses.addAll(statusesTrends);
}
statuses.pagination = new Pagination();
initializeStatusesCommonView(statuses);
});
}
}
};
mainHandler.post(myRunnable);
@ -905,21 +922,26 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
} else if (direction == DIRECTION.BOTTOM) {
if (networkAvailable == BaseMainActivity.status.CONNECTED) {
//We first if we get results from cache
timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null)
.observe(getViewLifecycleOwner(), statusesBottomCache -> {
if (statusesBottomCache != null && statusesBottomCache.statuses != null && statusesBottomCache.statuses.size() > 0) {
dealWithPagination(statusesBottomCache, DIRECTION.BOTTOM, false);
} else { // If not, we fetch remotely
timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false)
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
}
});
if (!fetchingMissing) {
if (networkAvailable == BaseMainActivity.status.CONNECTED) {
//We first if we get results from cache
timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null)
.observe(getViewLifecycleOwner(), statusesBottomCache -> {
if (statusesBottomCache != null && statusesBottomCache.statuses != null && statusesBottomCache.statuses.size() > 0) {
dealWithPagination(statusesBottomCache, DIRECTION.BOTTOM, false);
} else { // If not, we fetch remotely
timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false)
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
}
});
} else {
timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null)
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
}
} else {
timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null)
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id_fetch_more, null, null)
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, true));
}
} else if (direction == DIRECTION.TOP) {
if (!fetchingMissing) {
@ -966,7 +988,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
@Override
public void onClick(String min_id, String id) {
public void onClickMinId(String min_id, String id) {
//Fetch more has been pressed
min_id_fetch_more = min_id;
Status status = null;
@ -985,6 +1007,25 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
route(DIRECTION.TOP, true);
}
@Override
public void onClickMaxId(String max_id, String id) {
max_id_fetch_more = max_id;
Status status = null;
int position = 0;
for (Status currentStatus : this.statuses) {
if (currentStatus.id.compareTo(id) == 0) {
status = currentStatus;
break;
}
position++;
}
if (status != null) {
this.statuses.remove(position);
statusAdapter.notifyItemRemoved(position);
}
route(DIRECTION.BOTTOM, true);
}
public enum DIRECTION {
TOP,
BOTTOM,

@ -57,8 +57,9 @@ public class FragmentProfileTimeline extends Fragment {
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.media)));
binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(requireActivity(), R.attr.mTextColor), ContextCompat.getColor(requireActivity(), R.color.cyanea_accent_dark_reference));
binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(requireActivity()));
binding.viewpager.setAdapter(new FedilabProfilePageAdapter(requireActivity(), account));
binding.viewpager.setAdapter(new FedilabProfilePageAdapter(getChildFragmentManager(), account));
binding.viewpager.setOffscreenPageLimit(3);
binding.viewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout));
binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {

@ -16,11 +16,12 @@ package app.fedilab.android.ui.pageadapter;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.client.entities.app.BottomMenu;
@ -33,24 +34,46 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonConversation;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import app.fedilab.android.ui.fragment.timeline.FragmentNotificationContainer;
public class FedilabPageAdapter extends FragmentStateAdapter {
@SuppressWarnings("deprecation")
public class FedilabPageAdapter extends FragmentStatePagerAdapter {
public static final int BOTTOM_TIMELINE_COUNT = 5; //home, local, public, notification, DM
private final Pinned pinned;
private final BottomMenu bottomMenu;
private final int toRemove;
private Fragment mCurrentFragment;
public FedilabPageAdapter(BaseMainActivity activity, FragmentActivity fa, Pinned pinned, BottomMenu bottomMenu) {
super(fa);
public FedilabPageAdapter(BaseMainActivity activity, FragmentManager fm, Pinned pinned, BottomMenu bottomMenu) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.pinned = pinned;
this.bottomMenu = bottomMenu;
toRemove = PinnedTimelineHelper.itemToRemoveInBottomMenu(activity);
}
public Fragment getCurrentFragment() {
return mCurrentFragment;
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
@Override
public int getCount() {
if (pinned != null && pinned.pinnedTimelines != null) {
return pinned.pinnedTimelines.size() + BOTTOM_TIMELINE_COUNT - toRemove;
} else {
return BOTTOM_TIMELINE_COUNT - toRemove;
}
}
@NonNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
FragmentMastodonTimeline fragment = new FragmentMastodonTimeline();
Bundle bundle = new Bundle();
//Position 3 is for notifications
@ -95,12 +118,9 @@ public class FedilabPageAdapter extends FragmentStateAdapter {
}
@Override
public int getItemCount() {
if (pinned != null && pinned.pinnedTimelines != null) {
return pinned.pinnedTimelines.size() + BOTTOM_TIMELINE_COUNT - toRemove;
} else {
return BOTTOM_TIMELINE_COUNT - toRemove;
}
}
public int getItemPosition(@NonNull Object object) {
// POSITION_NONE makes it possible to reload the PagerAdapter
return POSITION_NONE;
}
}

@ -15,35 +15,42 @@ package app.fedilab.android.ui.pageadapter;
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import app.fedilab.android.client.entities.api.Account;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
public class FedilabProfilePageAdapter extends FragmentStateAdapter {
public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter {
private final Account account;
private Fragment mCurrentFragment;
public FedilabProfilePageAdapter(FragmentActivity fa, Account account) {
super(fa);
public FedilabProfilePageAdapter(FragmentManager fm, Account account) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.account = account;
}
public Fragment getCurrentFragment() {
return mCurrentFragment;
}
@Override
public int getItemCount() {
return 3;
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
@NonNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position);
switch (position) {
@ -77,5 +84,8 @@ public class FedilabProfilePageAdapter extends FragmentStateAdapter {
}
}
}
@Override
public int getCount() {
return 3;
}
}

@ -15,11 +15,12 @@ package app.fedilab.android.ui.pageadapter;
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import app.fedilab.android.client.entities.api.Account;
import app.fedilab.android.helper.Helper;
@ -27,21 +28,31 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import app.fedilab.android.ui.fragment.timeline.FragmentProfileTimeline;
public class FedilabProfileTLPageAdapter extends FragmentStateAdapter {
public class FedilabProfileTLPageAdapter extends FragmentStatePagerAdapter {
private final Account account;
private Fragment mCurrentFragment;
public FedilabProfileTLPageAdapter(FragmentActivity fa, Account account) {
super(fa);
public FedilabProfileTLPageAdapter(FragmentManager fm, Account account) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.account = account;
}
public Fragment getCurrentFragment() {
return mCurrentFragment;
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
@NonNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
switch (position) {
case 0:
FragmentProfileTimeline fragmentProfileTimeline = new FragmentProfileTimeline();
Bundle bundle = new Bundle();
@ -63,10 +74,9 @@ public class FedilabProfileTLPageAdapter extends FragmentStateAdapter {
}
@Override
public int getItemCount() {
public int getCount() {
return 3;
}
public enum follow_type {
FOLLOWING,
FOLLOWERS

@ -15,26 +15,39 @@ package app.fedilab.android.ui.pageadapter;
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.ui.fragment.timeline.FragmentScheduled;
public class FedilabScheduledPageAdapter extends FragmentStateAdapter {
public class FedilabScheduledPageAdapter extends FragmentStatePagerAdapter {
private Fragment mCurrentFragment;
public FedilabScheduledPageAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
public Fragment getCurrentFragment() {
return mCurrentFragment;
}
public FedilabScheduledPageAdapter(FragmentActivity fa) {
super(fa);
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
@NonNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position);
FragmentScheduled fragmentScheduled = new FragmentScheduled();
@ -53,7 +66,7 @@ public class FedilabScheduledPageAdapter extends FragmentStateAdapter {
}
@Override
public int getItemCount() {
public int getCount() {
return 3;
}
}
}

@ -38,6 +38,7 @@ import app.fedilab.android.client.entities.api.MastodonList;
import app.fedilab.android.client.entities.api.Pagination;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.api.Statuses;
import app.fedilab.android.client.entities.api.Tag;
import app.fedilab.android.client.entities.app.BaseAccount;
import app.fedilab.android.client.entities.app.StatusCache;
import app.fedilab.android.client.entities.app.StatusDraft;
@ -75,6 +76,8 @@ public class TimelinesVM extends AndroidViewModel {
private MutableLiveData<MastodonList> mastodonListMutableLiveData;
private MutableLiveData<List<MastodonList>> mastodonListListMutableLiveData;
private MutableLiveData<Marker> markerMutableLiveData;
private MutableLiveData<List<Status>> statusListMutableLiveData;
private MutableLiveData<List<Tag>> tagListMutableLiveData;
public TimelinesVM(@NonNull Application application) {
super(application);
@ -107,6 +110,56 @@ public class TimelinesVM extends AndroidViewModel {
return retrofit.create(MastodonTimelinesService.class);
}
public LiveData<List<Status>> getStatusTrends(String token, @NonNull String instance) {
MastodonTimelinesService mastodonTimelinesService = init(instance);
statusListMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
Call<List<Status>> publicTlCall = mastodonTimelinesService.getStatusTrends(token);
List<Status> statusList = null;
if (publicTlCall != null) {
try {
Response<List<Status>> publicTlResponse = publicTlCall.execute();
if (publicTlResponse.isSuccessful()) {
statusList = publicTlResponse.body();
statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList);
}
} catch (Exception e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
List<Status> finalStatusList = statusList;
Runnable myRunnable = () -> statusListMutableLiveData.setValue(finalStatusList);
mainHandler.post(myRunnable);
}).start();
return statusListMutableLiveData;
}
public LiveData<List<Tag>> getTagsTrends(String token, @NonNull String instance) {
MastodonTimelinesService mastodonTimelinesService = init(instance);
tagListMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
Call<List<Tag>> publicTlCall = mastodonTimelinesService.getTagTrends(token);
List<Tag> tagList = null;
if (publicTlCall != null) {
try {
Response<List<Tag>> publicTlResponse = publicTlCall.execute();
if (publicTlResponse.isSuccessful()) {
tagList = publicTlResponse.body();
}
} catch (Exception e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
List<Tag> finalTagList = tagList;
Runnable myRunnable = () -> tagListMutableLiveData.setValue(finalTagList);
mainHandler.post(myRunnable);
}).start();
return tagListMutableLiveData;
}
/**
* Public timeline
*

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,15L3,15v4c0,1.1 0.9,2 2,2h4v-2L5,19v-4zM5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2v4h2L5,5zM19,3h-4v2h4v4h2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19h-4v2h4c1.1,0 2,-0.9 2,-2v-4h-2v4zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</vector>

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,6l2.29,2.29 -4.88,4.88 -4,-4L2,16.59 3.41,18l6,-6 4,4 6.3,-6.29L22,12V6z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8l-1.41,-1.41L13,12.17V0h-2v12.17l-5.58,-5.59L4,8l8,8 8,-8z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,16l1.41,1.41L11,11.83V24h2V11.83l5.58,5.59L20,16l-8,-8 -8,8z" />
</vector>

@ -16,14 +16,27 @@
android:orientation="horizontal"
app:layout_constraintGuide_end="?attr/actionBarSize" />
<ja.burhanrashid52.photoeditor.PhotoEditorView
android:id="@+id/photoEditorView"
<RelativeLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/container_image"
app:layout_constraintTop_toTopOf="parent">
<ja.burhanrashid52.photoeditor.PhotoEditorView
android:id="@+id/photoEditorView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<app.fedilab.android.helper.CirclesDrawingView
android:id="@+id/focus_circle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</RelativeLayout>
<ImageView
android:id="@+id/imgUndo"
@ -34,7 +47,6 @@
android:src="@drawable/ic_undo"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintEnd_toStartOf="@+id/imgRedo" />
<ImageView
android:id="@+id/imgRedo"
android:layout_width="@dimen/top_tool_icon_width"

@ -75,19 +75,14 @@
</com.google.android.material.appbar.AppBarLayout>
<app.fedilab.android.helper.NestedScrollableHost
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true" />
</app.fedilab.android.helper.NestedScrollableHost>
android:layout_marginBottom="?attr/actionBarSize"
app:defaultNavHost="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

@ -29,7 +29,7 @@
android:animateLayoutChanges="true"
android:background="@android:color/transparent">
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/media_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"

@ -42,9 +42,9 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:id="@+id/profile_container"
android:layout_height="wrap_content"
android:paddingTop="?attr/actionBarSize"
app:layout_collapseMode="parallax">
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.card.MaterialCardView
android:id="@+id/banner_container"
@ -157,351 +157,186 @@
app:layout_constraintTop_toBottomOf="@id/account_dn"
tools:text="\@username\@instance.test" />
<LinearLayout
android:id="@+id/main_header_container"
android:layout_width="match_parent"
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/names_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="?actionBarSize"
android:paddingTop="8dp"
app:layout_collapseMode="parallax"
app:layout_constraintTop_toBottomOf="@id/account_un">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/account_un">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp">
<LinearLayout
android:id="@+id/names_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="@drawable/red_border"
android:text="Peertube"
android:textColor="@color/red_1"
android:visibility="gone"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/account_bot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="@drawable/blue_border"
android:text="@string/bot"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<TextView
android:id="@+id/temp_mute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/red"
android:visibility="gone" />
<TextView
android:id="@+id/account_moved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:drawablePadding="4dp"
android:gravity="center"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/identity_proofs_indicator"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginStart="10dp"
android:contentDescription="@string/identity_proofs"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_baseline_verified_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/names_container"
app:layout_constraintStart_toEndOf="@id/names_container"
app:layout_constraintTop_toTopOf="@id/names_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/account_note"
android:layout_width="match_parent"
android:layout_marginTop="5dp"
android:background="@drawable/red_border"
android:text="Peertube"
android:textColor="@color/red_1"
android:visibility="gone"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_bot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="@drawable/blue_border"
android:text="@string/bot"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/temp_mute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:textIsSelectable="true" />
android:textColor="@color/red"
android:visibility="gone" />
<TextView
android:id="@+id/personal_note"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_moved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:drawablePadding="5dp"
android:drawablePadding="4dp"
android:gravity="center"
android:padding="10dp"
android:text="@string/action_add_notes"
android:textIsSelectable="true"
app:drawableStartCompat="@drawable/ic_baseline_note_24" />
android:textSize="16sp"
android:visibility="gone" />
<HorizontalScrollView
android:layout_width="match_parent"
</androidx.appcompat.widget.LinearLayoutCompat>
<ImageView
android:id="@+id/identity_proofs_indicator"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginStart="10dp"
android:contentDescription="@string/identity_proofs"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_baseline_verified_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/names_container"
app:layout_constraintStart_toEndOf="@id/names_container"
app:layout_constraintTop_toTopOf="@id/names_container" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center"
android:padding="10dp"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/names_container" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/personal_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:drawablePadding="5dp"
android:gravity="center"
android:padding="10dp"
android:text="@string/action_add_notes"
android:textIsSelectable="true"
app:drawableStartCompat="@drawable/ic_baseline_note_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/account_note" />
<HorizontalScrollView
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:fillViewport="true"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/personal_note">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:fillViewport="true"
android:scrollbars="none">
android:gravity="center_horizontal"
android:orientation="horizontal">
<LinearLayout
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/instance_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:id="@+id/instance_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<TextView
android:id="@+id/account_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<TextView
android:id="@+id/account_followed_by"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/green_border"
android:singleLine="true"
android:text="@string/followed_by"
android:textColor="@color/verified_text"
android:visibility="gone" />
<TextView
android:id="@+id/account_follow_request"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:text="@string/request_sent"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
</LinearLayout>
</HorizontalScrollView>
<!-- Fields container -->
<LinearLayout
android:id="@+id/fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<!-- Fields 1 to 4 -->
<LinearLayout
android:id="@+id/field1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/label1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:minHeight="20dp"
android:padding="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
<LinearLayout
android:id="@+id/value1BG"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/value1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/field2"
android:layout_width="match_parent"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/label2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:minHeight="20dp"
android:padding="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp" />
<LinearLayout
android:id="@+id/value2BG"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/value2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/field3"
android:layout_width="match_parent"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_followed_by"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/label3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:minHeight="20dp"
android:padding="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp" />
<LinearLayout
android:id="@+id/value3BG"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/value3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/field4"
android:layout_width="match_parent"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/green_border"
android:singleLine="true"
android:text="@string/followed_by"
android:textColor="@color/verified_text"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/account_follow_request"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/label4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:minHeight="20dp"
android:padding="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp" />
<LinearLayout
android:id="@+id/value4BG"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/value4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- End Fields container -->
<TextView
android:id="@+id/warning_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/disclaimer_full"
android:textColor="@color/dark_text"
android:visibility="gone" />
</LinearLayout>
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="@drawable/blue_border"
android:singleLine="true"
android:text="@string/request_sent"
android:textColor="@color/mastodonC4"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>
</HorizontalScrollView>
<!-- Fields container -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/info" />
<!-- End Fields container -->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/warning_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/disclaimer_full"
android:textColor="@color/dark_text"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fields_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -510,6 +345,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:backgroundTint="?colorPrimaryDark"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_collapseMode="pin">
<androidx.appcompat.widget.AppCompatImageView
@ -532,14 +368,6 @@
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.tabs.TabLayout
android:id="@+id/account_tabLayout"
android:layout_width="match_parent"
@ -547,11 +375,12 @@
android:background="?backgroundColorLight"
app:tabGravity="fill"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/account_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/account_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -63,7 +63,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/schedule_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"

@ -30,15 +30,9 @@
app:tabIndicatorColor="@color/cyanea_accent_dark_reference"
app:tabMode="scrollable" />
<app.fedilab.android.helper.NestedScrollableHost
<androidx.viewpager.widget.ViewPager
android:id="@+id/search_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/search_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</app.fedilab.android.helper.NestedScrollableHost>
>
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 Thomas Schneider
This file is a part of Fedilab
This program is free software; you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software Foundation; either version 3 of the
License, or (at your option) any later version.
Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details.
You should have received a copy of the GNU General Public License along with Fedilab; if not,
see <http://www.gnu.org/licenses>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/search_tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?backgroundColorLight"
app:tabGravity="fill"
app:tabIndicatorColor="@color/cyanea_accent_dark_reference"
app:tabMode="fixed" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/trends_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>

@ -1,9 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fetch_more"
style="@style/MyOutlinedButton"
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fetch_more_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="6dp"
android:text="@string/fetch_more_messages" />
android:orientation="horizontal"
android:paddingHorizontal="6dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/fetch_more_min"
style="@style/Widget.App.Button.IconOnly.Outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/fetch_more_messages"
app:icon="@drawable/ic_fetch_more_arrow_upward"
app:iconPadding="0dp" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="6dp"
android:layout_weight="1"
android:text="@string/fetch_more_messages"
android:textAlignment="center"
android:textColor="?colorAccent"
android:textSize="18sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fetch_more_max"
style="@style/Widget.App.Button.IconOnly.Outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6dp"
android:layout_marginBottom="6dp"
android:contentDescription="@string/fetch_more_messages"
app:icon="@drawable/ic_fetch_more_arrow_downward"
app:iconPadding="0dp" />
</androidx.appcompat.widget.LinearLayoutCompat>

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/field1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/label"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:minHeight="20dp"
android:padding="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/valueBG"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/value"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textIsSelectable="true" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

@ -27,7 +27,7 @@
app:tabGravity="fill"
app:tabMaxWidth="0dp" />
<androidx.viewpager2.widget.ViewPager2
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

@ -35,6 +35,12 @@
android:title="@string/action_announcements"
android:visible="true" />
<item
android:id="@+id/nav_trends"
android:icon="@drawable/ic_baseline_trending_up_24"
android:title="@string/trending"
android:visible="true" />
<item
android:id="@+id/nav_scheduled"
android:icon="@drawable/ic_baseline_schedule_24"

@ -17,15 +17,15 @@
<string name="password">Пароль</string>
<string name="email">Ел. пошта</string>
<string name="accounts">Облікові записи</string>
<string name="toots">Передмухнуте</string>
<string name="toots">Повідомлення</string>
<string name="tags">Мітки</string>
<string name="save">Зберегти</string>
<string name="instance">Екземпляр</string>
<string name="instance_example">Екземпляр: mastodon.social</string>
<string name="toast_account_changed" formatted="false">Тепер працює з обліковим записом %1$s</string>
<string name="add_account">Додати обліковий запис</string>
<string name="clipboard">Вміст дмуху скопійований в буфер обміну</string>
<string name="clipboard_url">The URL of the toot has been copied to the clipboard</string>
<string name="clipboard">Вміст повідомлення скопійований в буфер обміну</string>
<string name="clipboard_url">URL-адресу повідомлення скопійовано в буфер обміну</string>
<string name="camera">Камера</string>
<string name="delete_all">Видалити все</string>
<string name="schedule">Запланувати</string>
@ -57,17 +57,17 @@
<string name="follow_request">Запити на підписку</string>
<string name="settings">Налаштування</string>
<string name="send_email">Надіслати ел. лист</string>
<string name="scheduled_toots">Заплановані дмухи</string>
<string name="scheduled_toots">Заплановані повідомлення</string>
<string name="disclaimer_full">Наведена нижче інформація може відображати профіль користувача не повністю.</string>
<string name="insert_emoji">Вставити смайли</string>
<string name="no_emoji">Застосунок не збирає emojis на даний момент.</string>
<string name="logout_account_confirmation">Are you sure you want to logout @%1$s@%2$s?</string>
<!-- Status -->
<string name="no_status">Немає повідомлень для відображення</string>
<string name="favourite_add">Додати цей дмух до обраного?</string>
<string name="favourite_remove">Видалити цей дмух з обраного?</string>
<string name="reblog_add">Передмухнути?</string>
<string name="reblog_remove">Зняти передмух?</string>
<string name="favourite_add">Додати це повідомлення до обраного\?</string>
<string name="favourite_remove">Видалити це повідомлення з обраного\?</string>
<string name="reblog_add">Підштовхнути це повідомлення\?</string>
<string name="reblog_remove">Не підштовхувати це повідомлення\?</string>
<string name="more_action_1">Заглушити</string>
<string name="more_action_2">Заблокувати</string>
<string name="more_action_3">Повідомити про порушення</string>
@ -130,8 +130,8 @@
<!-- TOOT -->
<string name="toot_select_image_error">Сталася помилка під час вибору медіа-файлу!</string>
<string name="toot_delete_media">Видалити цей медіа-файл?</string>
<string name="toot_error_no_content">Ваш дмух пустий!</string>
<string name="toot_sent">Повідомлення було відправлено!</string>
<string name="toot_error_no_content">Ваше повідомлення порожнє!</string>
<string name="toot_sent">Повідомлення відправлено!</string>
<string name="toot_sensitive">Чутливий вміст?</string>
<string name="no_draft">Чернетки відсутні!</string>
<string name="choose_accounts">Оберіть обліковий запис</string>
@ -151,14 +151,15 @@
<!-- Accounts -->
<string name="no_accounts">Немає облікових записів для відображення</string>
<string name="no_follow_request">Запит на відмову від підписки</string>
<string name="status_cnt">Дмухи \n %1$s</string>
<string name="status_cnt">Повідомлення
\n %1$s</string>
<string name="following_cnt">Підписані на \n %1$s</string>
<string name="followers_cnt">Стежать за вами \n %1$s</string>
<string name="reject">Відхилити</string>
<!-- Scheduled toots -->
<string name="no_scheduled_toots">Немає запланованих дмухів для показу!</string>
<string name="remove_scheduled">Видалити запланований дмух?</string>
<string name="toot_scheduled">Дмух було заплановано!</string>
<string name="no_scheduled_toots">Немає запланованих повідомлень для показу!</string>
<string name="remove_scheduled">Видалити заплановане повідомлення\?</string>
<string name="toot_scheduled">Повідомлення заплановано!</string>
<string name="toot_scheduled_date">Запланована дата має бути більшою за поточну годину!</string>
<!-- timed mute -->
<string name="timed_mute_date_error">Час для приглушення має бути більшим за одну хвилину.</string>
@ -184,10 +185,10 @@
<string name="toast_unmute">Обліковий запис більше не приглушено!</string>
<string name="toast_follow">На обліковий запис підписалися!</string>
<string name="toast_unfollow">Ви більше не підписані на обліковий запис!</string>
<string name="toast_reblog">Передмухнуто!</string>
<string name="toast_unreblog">Більше не передмухнуто!</string>
<string name="toast_favourite">Дмух додано до обраного!</string>
<string name="toast_unfavourite">Дмух видалено з обраного!</string>
<string name="toast_reblog">Повідомлення підштовхнуте!</string>
<string name="toast_unreblog">Повідомлення більше не підштовхнуте!</string>
<string name="toast_favourite">Повідомлення додано до обраного!</string>
<string name="toast_unfavourite">Повідомлення видалено з обраного!</string>
<string name="toast_error">Вибачте, виникла помилка!</string>
<string name="toast_code_error">Сталася помилка! Екземпляр не повернув код авторизації!</string>
<string name="toast_error_instance">Домен екземпляру не валідний!</string>
@ -196,7 +197,7 @@
<string name="nothing_to_do">Ніяка дія не може відбутися</string>
<string name="toast_error_translate">Виникла помилка під час перекладу!</string>
<!-- Settings -->
<string name="set_toots_page">Число дмухів за одиницю завантаження</string>
<string name="set_toots_page">Кількість повідомлень за одиницю завантаження</string>
<string name="set_disable_gif">Вимкнути GIF аватари</string>
<string name="set_notif_follow">Сповіщати, якщо хтось підписується</string>
<string name="set_notif_follow_share">Сповіщати, якщо хтось передмухнув ваш статус</string>
@ -275,7 +276,7 @@
<string name="poxy_port">Порт</string>
<string name="poxy_login">Логін</string>
<string name="poxy_password">Пароль</string>
<string name="set_share_details">Додати деталі гудку при поділитися</string>
<string name="set_share_details">Додати деталі повідомлення при поширюванні</string>
<string name="support_the_app_on_liberapay">Підтримати додаток на Liberapay</string>
<string name="alert_regex">Помилка у регулярному виразі!</string>
<string name="toast_instance_unavailable">No timelines was found on this instance!</string>
@ -311,7 +312,7 @@
<string name="channel_notif_fav">New Favourite</string>
<string name="channel_notif_mention">New Mention</string>
<string name="channel_notif_poll">Poll Ended</string>
<string name="channel_notif_backup">Toots Backup</string>
<string name="channel_notif_backup">Запасне копіювання повідомлень</string>
<string name="channel_notif_status">New posts</string>
<string name="channel_notif_media">Медіа завантаження</string>
<string name="select_sound">Оберіть сигнал</string>
@ -323,11 +324,11 @@
<string name="peertube_instance">Peertube екземпляр</string>
<string name="set_display_emoji">Use Emoji One</string>
<string name="information">Information</string>
<string name="set_display_card">Display previews in all toots</string>
<string name="set_display_card">Відображати дрібнообрази в усіх повідомленнях</string>
<string name="account_id_clipbloard">The account id has been copied in the clipboard!</string>
<string name="set_change_locale">Change the language</string>
<string name="truncate_long_toots">Truncate long toots</string>
<string name="set_truncate_toot">Truncate toots over \'x\' lines. Zero means disabled.</string>
<string name="truncate_long_toots">Обтинати довгі повідомлення</string>
<string name="set_truncate_toot">Обтинати повідомлення більші, ніж \'x\' рядків. Нуль означає вимикання.</string>
<string name="display_toot_truncate">Display more</string>
<string name="hide_toot_truncate">Display less</string>
<string name="tags_already_stored">The tag already exists!</string>
@ -373,8 +374,8 @@
<string name="category">Category</string>
<string name="description">Description</string>
<string name="share">Share</string>
<string name="toots_server">Toots (Server)</string>
<string name="toots_client">Toots (Device)</string>
<string name="toots_server">Повідомлення (сервер)</string>
<string name="toots_client">Повідомлення (пристрій)</string>
<string name="settings_category_label_timelines">Timelines</string>
<string name="settings_category_label_interface">Interface</string>
<string name="contact">Contacts</string>
@ -396,7 +397,7 @@
<string name="poll_finish_at">end at %s</string>
<string name="vote">Vote</string>
<string name="notif_poll">A poll you have voted in has ended</string>
<string name="notif_poll_self">A poll you tooted has ended</string>
<string name="notif_poll_self">Опубліковане вами опитування закінчилося</string>
<string name="settings_category_notif_categories">Categories</string>
<string name="move_timeline">Move timeline</string>
<string name="hide_timeline">Hide timeline</string>
@ -531,7 +532,7 @@
<string name="background_status">Background color of posts in timelines</string>
<string name="reset_color">Reset colors</string>
<string name="clik_reset">Tap here to reset all your custom colors</string>
<string name="reset">Reset</string>
<string name="reset">Скинути</string>
<string name="icons_color_title">Icons</string>
<string name="icons_color">Color of bottom icons in timelines</string>
<string name="logo_of_the_instance">Logo of the instance</string>

@ -27,7 +27,7 @@
app:title="@string/embedded_browser" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="@string/SET_SEND_CRASH_REPORTS"
app:singleLineTitle="false"

@ -2,7 +2,7 @@
<paths>
<external-path
name="my_images"
path="Android/data/app.fedilab.android.test/files/Pictures" />
path="Android/data/app.fedilab.android/files/Pictures" />
<cache-path
name="*"

@ -0,0 +1,13 @@
Added:
- Allow to set a focus point on previews (media editor)
- Respect the focus point with previews
- Pagination with the fetch more button support reading up or down
- Add trends
Fixed:
- Only last push notification is displayed (not grouped)
- Bad behavior with the right/left scroll
- Fix long profiles not fully displayed
- Issues with some polls
- Some crashes
- Some bad behaviors
Loading…
Cancel
Save