mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2024-12-23 01:00:04 +02:00
Announcements
This commit is contained in:
parent
5d9268653c
commit
890b9b64a6
23 changed files with 1322 additions and 15 deletions
|
@ -150,6 +150,9 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.HashTagActivity"
|
android:name=".activities.HashTagActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize" />
|
android:configChanges="keyboardHidden|orientation|screenSize" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.AnnouncementActivity"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MediaActivity"
|
android:name=".activities.MediaActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
|
|
|
@ -80,6 +80,7 @@ import java.util.regex.Pattern;
|
||||||
import app.fedilab.android.activities.AboutActivity;
|
import app.fedilab.android.activities.AboutActivity;
|
||||||
import app.fedilab.android.activities.ActionActivity;
|
import app.fedilab.android.activities.ActionActivity;
|
||||||
import app.fedilab.android.activities.AdminActionActivity;
|
import app.fedilab.android.activities.AdminActionActivity;
|
||||||
|
import app.fedilab.android.activities.AnnouncementActivity;
|
||||||
import app.fedilab.android.activities.BaseActivity;
|
import app.fedilab.android.activities.BaseActivity;
|
||||||
import app.fedilab.android.activities.ComposeActivity;
|
import app.fedilab.android.activities.ComposeActivity;
|
||||||
import app.fedilab.android.activities.ContextActivity;
|
import app.fedilab.android.activities.ContextActivity;
|
||||||
|
@ -99,6 +100,8 @@ import app.fedilab.android.activities.ScheduledActivity;
|
||||||
import app.fedilab.android.activities.SearchResultTabActivity;
|
import app.fedilab.android.activities.SearchResultTabActivity;
|
||||||
import app.fedilab.android.activities.SettingsActivity;
|
import app.fedilab.android.activities.SettingsActivity;
|
||||||
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
|
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
|
||||||
|
import app.fedilab.android.client.entities.api.Emoji;
|
||||||
|
import app.fedilab.android.client.entities.api.EmojiInstance;
|
||||||
import app.fedilab.android.client.entities.api.Filter;
|
import app.fedilab.android.client.entities.api.Filter;
|
||||||
import app.fedilab.android.client.entities.api.Instance;
|
import app.fedilab.android.client.entities.api.Instance;
|
||||||
import app.fedilab.android.client.entities.api.MastodonList;
|
import app.fedilab.android.client.entities.api.MastodonList;
|
||||||
|
@ -128,6 +131,7 @@ import es.dmoral.toasty.Toasty;
|
||||||
public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener {
|
public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener {
|
||||||
|
|
||||||
public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software;
|
public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software;
|
||||||
|
public static List<Emoji> emojis;
|
||||||
public static Account.API api;
|
public static Account.API api;
|
||||||
public static boolean admin;
|
public static boolean admin;
|
||||||
public static WeakReference<Account> accountWeakReference;
|
public static WeakReference<Account> accountWeakReference;
|
||||||
|
@ -351,6 +355,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
||||||
} else if (id == R.id.nav_partnership) {
|
} else if (id == R.id.nav_partnership) {
|
||||||
Intent intent = new Intent(this, PartnerShipActivity.class);
|
Intent intent = new Intent(this, PartnerShipActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
} else if (id == R.id.nav_announcements) {
|
||||||
|
Intent intent = new Intent(this, AnnouncementActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
binding.drawerLayout.close();
|
binding.drawerLayout.close();
|
||||||
return false;
|
return false;
|
||||||
|
@ -724,6 +731,15 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
||||||
binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE));
|
binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE));
|
||||||
//For receiving data from other activities
|
//For receiving data from other activities
|
||||||
LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA));
|
LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA));
|
||||||
|
if (emojis == null) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
emojis = new EmojiInstance(BaseMainActivity.this).getEmojiList(BaseMainActivity.currentInstance);
|
||||||
|
} catch (DBException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -856,6 +872,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
popup.show();
|
popup.show();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshFragment() {
|
public void refreshFragment() {
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
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 static app.fedilab.android.BaseMainActivity.emojis;
|
||||||
|
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import app.fedilab.android.BaseMainActivity;
|
||||||
|
import app.fedilab.android.R;
|
||||||
|
import app.fedilab.android.client.entities.api.EmojiInstance;
|
||||||
|
import app.fedilab.android.databinding.ActivityAnnouncementBinding;
|
||||||
|
import app.fedilab.android.exception.DBException;
|
||||||
|
import app.fedilab.android.helper.Helper;
|
||||||
|
import app.fedilab.android.helper.ThemeHelper;
|
||||||
|
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAnnouncement;
|
||||||
|
|
||||||
|
|
||||||
|
public class AnnouncementActivity extends BaseActivity {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
ThemeHelper.applyTheme(this);
|
||||||
|
ActivityAnnouncementBinding binding = ActivityAnnouncementBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar);
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
//Remove title
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayShowTitleEnabled(false);
|
||||||
|
actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
|
||||||
|
}
|
||||||
|
binding.title.setText(R.string.action_announcements);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_tags, new FragmentMastodonAnnouncement(), null, null, null);
|
||||||
|
if (emojis == null) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
emojis = new EmojiInstance(AnnouncementActivity.this).getEmojiList(BaseMainActivity.currentInstance);
|
||||||
|
} catch (DBException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == android.R.id.home) {
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,28 +30,27 @@ import retrofit2.http.Query;
|
||||||
|
|
||||||
public interface MastodonAnnouncementsService {
|
public interface MastodonAnnouncementsService {
|
||||||
|
|
||||||
@GET("/announcements")
|
@GET("announcements")
|
||||||
Call<List<Announcement>> getAnnouncements(
|
Call<List<Announcement>> getAnnouncements(
|
||||||
@Header("Authorization") String token,
|
@Header("Authorization") String token,
|
||||||
@Query("with_dismissed") boolean with_dismissed
|
@Query("with_dismissed") Boolean with_dismissed
|
||||||
);
|
);
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("/announcements/{id}/dismiss")
|
@POST("announcements/{id}/dismiss")
|
||||||
Call<Void> dismiss(
|
Call<Void> dismiss(
|
||||||
@Header("Authorization") String app_token,
|
@Header("Authorization") String app_token,
|
||||||
@Path("id") String id
|
@Path("id") String id
|
||||||
);
|
);
|
||||||
|
|
||||||
@FormUrlEncoded
|
@PUT("announcements/{id}/reactions/{name}")
|
||||||
@PUT("/announcements/{id}/reactions/{name}")
|
|
||||||
Call<Void> addReaction(
|
Call<Void> addReaction(
|
||||||
@Header("Authorization") String app_token,
|
@Header("Authorization") String app_token,
|
||||||
@Path("id") String id,
|
@Path("id") String id,
|
||||||
@Path("name") String name
|
@Path("name") String name
|
||||||
);
|
);
|
||||||
|
|
||||||
@DELETE("/announcements/{id}/reactions/{name}")
|
@DELETE("announcements/{id}/reactions/{name}")
|
||||||
Call<Void> removeReaction(
|
Call<Void> removeReaction(
|
||||||
@Header("Authorization") String app_token,
|
@Header("Authorization") String app_token,
|
||||||
@Path("id") String id,
|
@Path("id") String id,
|
||||||
|
|
|
@ -14,6 +14,8 @@ package app.fedilab.android.client.entities.api;
|
||||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
import android.text.Spannable;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -46,4 +48,7 @@ public class Announcement {
|
||||||
public List<Emoji> emojis;
|
public List<Emoji> emojis;
|
||||||
@SerializedName("reactions")
|
@SerializedName("reactions")
|
||||||
public List<Reaction> reactions;
|
public List<Reaction> reactions;
|
||||||
|
|
||||||
|
//Some extra spannable element - They will be filled automatically when fetching the status
|
||||||
|
public transient Spannable span_content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -602,7 +602,6 @@ public class Helper {
|
||||||
public static void removeAccount(Activity activity, Account account) throws DBException {
|
public static void removeAccount(Activity activity, Account account) throws DBException {
|
||||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
//Current user
|
//Current user
|
||||||
SQLiteDatabase db = Sqlite.getInstance(activity.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
|
|
||||||
String userId = sharedpreferences.getString(PREF_USER_ID, null);
|
String userId = sharedpreferences.getString(PREF_USER_ID, null);
|
||||||
String instance = sharedpreferences.getString(PREF_USER_INSTANCE, null);
|
String instance = sharedpreferences.getString(PREF_USER_INSTANCE, null);
|
||||||
Account accountDB = new Account(activity);
|
Account accountDB = new Account(activity);
|
||||||
|
@ -881,6 +880,34 @@ public class Helper {
|
||||||
return Cyanea.getInstance().isDark() ? R.style.PopupDark : R.style.Popup;
|
return Cyanea.getInstance().isDark() ? R.style.PopupDark : R.style.Popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a media into a view
|
||||||
|
*
|
||||||
|
* @param view ImageView - the view where the image will be loaded
|
||||||
|
* @param url - String
|
||||||
|
*/
|
||||||
|
public static void loadImage(ImageView view, String url) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
|
||||||
|
if (disableGif || (!url.endsWith(".gif"))) {
|
||||||
|
Glide.with(view.getContext())
|
||||||
|
.asDrawable()
|
||||||
|
.load(url)
|
||||||
|
.thumbnail(0.1f)
|
||||||
|
.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10)))
|
||||||
|
.into(view);
|
||||||
|
} else {
|
||||||
|
Glide.with(view.getContext())
|
||||||
|
.asGif()
|
||||||
|
.load(url)
|
||||||
|
.thumbnail(0.1f)
|
||||||
|
.apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners(10)))
|
||||||
|
.into(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a profile picture for the account
|
* Load a profile picture for the account
|
||||||
*
|
*
|
||||||
|
|
|
@ -76,6 +76,7 @@ import app.fedilab.android.activities.HashTagActivity;
|
||||||
import app.fedilab.android.activities.MainActivity;
|
import app.fedilab.android.activities.MainActivity;
|
||||||
import app.fedilab.android.activities.ProfileActivity;
|
import app.fedilab.android.activities.ProfileActivity;
|
||||||
import app.fedilab.android.client.entities.api.Account;
|
import app.fedilab.android.client.entities.api.Account;
|
||||||
|
import app.fedilab.android.client.entities.api.Announcement;
|
||||||
import app.fedilab.android.client.entities.api.Attachment;
|
import app.fedilab.android.client.entities.api.Attachment;
|
||||||
import app.fedilab.android.client.entities.api.Emoji;
|
import app.fedilab.android.client.entities.api.Emoji;
|
||||||
import app.fedilab.android.client.entities.api.Field;
|
import app.fedilab.android.client.entities.api.Field;
|
||||||
|
@ -553,6 +554,426 @@ public class SpannableHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML content to text. Also, it handles click on link and transform emoji
|
||||||
|
* This needs to be run asynchronously
|
||||||
|
*
|
||||||
|
* @param context {@link Context}
|
||||||
|
* @param announcement {@link Announcement} - Announcement concerned by the spannable transformation
|
||||||
|
* @param text String - text to convert, it can be content, spoiler, poll items, etc.
|
||||||
|
* @return Spannable string
|
||||||
|
*/
|
||||||
|
private static Spannable convert(@NonNull Context context, @NonNull Announcement announcement, String text) {
|
||||||
|
SpannableString initialContent;
|
||||||
|
if (text == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Matcher matcherALink = Helper.aLink.matcher(text);
|
||||||
|
//We stock details
|
||||||
|
HashMap<String, String> urlDetails = new HashMap<>();
|
||||||
|
while (matcherALink.find()) {
|
||||||
|
String urlText = matcherALink.group(3);
|
||||||
|
String url = matcherALink.group(2);
|
||||||
|
if (urlText != null) {
|
||||||
|
urlText = urlText.substring(1);
|
||||||
|
}
|
||||||
|
if (url != null && urlText != null && !url.equals(urlText) && !urlText.contains("<span")) {
|
||||||
|
urlDetails.put(url, urlText);
|
||||||
|
text = text.replaceAll(Pattern.quote(matcherALink.group()), Matcher.quoteReplacement(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||||
|
initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
|
||||||
|
else
|
||||||
|
initialContent = new SpannableString(Html.fromHtml(text));
|
||||||
|
|
||||||
|
SpannableStringBuilder content = new SpannableStringBuilder(initialContent);
|
||||||
|
URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class);
|
||||||
|
for (URLSpan span : urls) {
|
||||||
|
content.removeSpan(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--- EMOJI ----
|
||||||
|
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
|
||||||
|
List<Emoji> emojiList = announcement.emojis;
|
||||||
|
//Will convert emoji if asked
|
||||||
|
if (emojiList != null && emojiList.size() > 0) {
|
||||||
|
for (Emoji emoji : emojiList) {
|
||||||
|
if (Helper.isValidContextForGlide(context)) {
|
||||||
|
FutureTarget<File> futureTarget = Glide.with(context)
|
||||||
|
.asFile()
|
||||||
|
.load(disableGif ? emoji.static_url : emoji.url)
|
||||||
|
.submit();
|
||||||
|
try {
|
||||||
|
File file = futureTarget.get();
|
||||||
|
final String targetedEmoji = ":" + emoji.shortcode + ":";
|
||||||
|
if (content.toString().contains(targetedEmoji)) {
|
||||||
|
//emojis can be used several times so we have to loop
|
||||||
|
for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) {
|
||||||
|
final int endPosition = startPosition + targetedEmoji.length();
|
||||||
|
if (endPosition <= content.toString().length() && endPosition >= startPosition) {
|
||||||
|
ImageSpan imageSpan;
|
||||||
|
if (APNGParser.isAPNG(file.getAbsolutePath())) {
|
||||||
|
APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath());
|
||||||
|
try {
|
||||||
|
apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
|
||||||
|
apngDrawable.setVisible(true, true);
|
||||||
|
imageSpan = new ImageSpan(apngDrawable);
|
||||||
|
if (endPosition <= content.length()) {
|
||||||
|
content.setSpan(
|
||||||
|
imageSpan, startPosition,
|
||||||
|
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} else if (GifParser.isGif(file.getAbsolutePath())) {
|
||||||
|
GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath());
|
||||||
|
try {
|
||||||
|
gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
|
||||||
|
gifDrawable.setVisible(true, true);
|
||||||
|
imageSpan = new ImageSpan(gifDrawable);
|
||||||
|
if (endPosition <= content.length()) {
|
||||||
|
content.setSpan(
|
||||||
|
imageSpan, startPosition,
|
||||||
|
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Drawable drawable = Drawable.createFromPath(file.getAbsolutePath());
|
||||||
|
try {
|
||||||
|
drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
|
||||||
|
drawable.setVisible(true, true);
|
||||||
|
imageSpan = new ImageSpan(drawable);
|
||||||
|
if (endPosition <= content.length()) {
|
||||||
|
content.setSpan(
|
||||||
|
imageSpan, startPosition,
|
||||||
|
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--- URLs ----
|
||||||
|
Matcher matcherLink = Patterns.WEB_URL.matcher(content);
|
||||||
|
int offSetTruncate = 0;
|
||||||
|
while (matcherLink.find()) {
|
||||||
|
int matchStart = matcherLink.start() - offSetTruncate;
|
||||||
|
int matchEnd = matchStart + matcherLink.group().length();
|
||||||
|
if (matchEnd > content.toString().length()) {
|
||||||
|
matchEnd = content.toString().length();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String url = content.toString().substring(matchStart, matchEnd);
|
||||||
|
String newURL = Helper.transformURL(context, url);
|
||||||
|
//If URL has been transformed
|
||||||
|
if (newURL.compareTo(url) != 0) {
|
||||||
|
content.replace(matchStart, matchEnd, newURL);
|
||||||
|
offSetTruncate += (newURL.length() - url.length());
|
||||||
|
matchEnd = matchStart + newURL.length();
|
||||||
|
//The transformed URL was in the list of URLs having a different names
|
||||||
|
if (urlDetails.containsKey(url)) {
|
||||||
|
urlDetails.put(newURL, urlDetails.get(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Truncate URL if needed
|
||||||
|
//TODO: add an option to disable truncated URLs
|
||||||
|
String urlText = newURL;
|
||||||
|
if (newURL.length() > 30 && !urlDetails.containsKey(urlText)) {
|
||||||
|
urlText = urlText.substring(0, 30);
|
||||||
|
urlText += "…";
|
||||||
|
content.replace(matchStart, matchEnd, urlText);
|
||||||
|
matchEnd = matchStart + 31;
|
||||||
|
offSetTruncate += (newURL.length() - urlText.length());
|
||||||
|
} else if (urlDetails.containsKey(urlText) && urlDetails.get(urlText) != null) {
|
||||||
|
urlText = urlDetails.get(urlText);
|
||||||
|
if (urlText != null) {
|
||||||
|
content.replace(matchStart, matchEnd, urlText);
|
||||||
|
matchEnd = matchStart + urlText.length();
|
||||||
|
offSetTruncate += (newURL.length() - urlText.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchEnd <= content.length() && matchEnd >= matchStart) {
|
||||||
|
content.setSpan(new LongClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onLongClick(View view) {
|
||||||
|
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(view.getContext(), 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());
|
||||||
|
builder.setMessage(url);
|
||||||
|
builder.setTitle(context.getString(R.string.display_full_link));
|
||||||
|
builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
|
||||||
|
.show();
|
||||||
|
alertDialog.dismiss();
|
||||||
|
});
|
||||||
|
popupLinksBinding.shareLink.setOnClickListener(v -> {
|
||||||
|
Intent sendIntent = new Intent(Intent.ACTION_SEND);
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_TEXT, url);
|
||||||
|
sendIntent.setType("text/plain");
|
||||||
|
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with));
|
||||||
|
intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intentChooser);
|
||||||
|
alertDialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
popupLinksBinding.openOtherApp.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse(url));
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
try {
|
||||||
|
context.startActivity(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
alertDialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
popupLinksBinding.copyLink.setOnClickListener(v -> {
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url);
|
||||||
|
if (clipboard != null) {
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
alertDialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
popupLinksBinding.checkRedirect.setOnClickListener(v -> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
URL finalUrlCheck = new URL(url);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String redirect = null;
|
||||||
|
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection();
|
||||||
|
httpsURLConnection.setConnectTimeout(10 * 1000);
|
||||||
|
httpsURLConnection.setRequestProperty("http.keepAlive", "false");
|
||||||
|
httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT);
|
||||||
|
httpsURLConnection.setRequestMethod("HEAD");
|
||||||
|
if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) {
|
||||||
|
Map<String, List<String>> map = httpsURLConnection.getHeaderFields();
|
||||||
|
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
|
||||||
|
if (entry.toString().toLowerCase().startsWith("location")) {
|
||||||
|
Matcher matcher = urlPattern.matcher(entry.toString());
|
||||||
|
if (matcher.find()) {
|
||||||
|
redirect = matcher.group(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpsURLConnection.getInputStream().close();
|
||||||
|
if (redirect != null && redirect.compareTo(url) != 0) {
|
||||||
|
URL redirectURL = new URL(redirect);
|
||||||
|
String host = redirectURL.getHost();
|
||||||
|
String protocol = redirectURL.getProtocol();
|
||||||
|
if (protocol == null || host == null) {
|
||||||
|
redirect = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Handler mainHandler = new Handler(context.getMainLooper());
|
||||||
|
String finalRedirect = redirect;
|
||||||
|
Runnable myRunnable = () -> {
|
||||||
|
AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
|
||||||
|
if (finalRedirect != null) {
|
||||||
|
builder1.setMessage(context.getString(R.string.redirect_detected, url, finalRedirect));
|
||||||
|
builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> {
|
||||||
|
ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect);
|
||||||
|
if (clipboard1 != null) {
|
||||||
|
clipboard1.setPrimaryClip(clip1);
|
||||||
|
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
builder1.setNeutralButton(R.string.share_link, (dialog, which) -> {
|
||||||
|
Intent sendIntent1 = new Intent(Intent.ACTION_SEND);
|
||||||
|
sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
|
||||||
|
sendIntent1.putExtra(Intent.EXTRA_TEXT, url);
|
||||||
|
sendIntent1.setType("text/plain");
|
||||||
|
context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with)));
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
builder1.setMessage(R.string.no_redirect);
|
||||||
|
}
|
||||||
|
builder1.setTitle(context.getString(R.string.check_redirect));
|
||||||
|
builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
|
||||||
|
.show();
|
||||||
|
|
||||||
|
};
|
||||||
|
mainHandler.post(myRunnable);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}).start();
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
alertDialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View textView) {
|
||||||
|
textView.setTag(CLICKABLE_SPAN);
|
||||||
|
Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
|
||||||
|
Matcher matcherLink = link.matcher(url);
|
||||||
|
if (matcherLink.find() && !url.contains("medium.com")) {
|
||||||
|
if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot
|
||||||
|
CrossActionHelper.fetchRemoteStatus(context, MainActivity.accountWeakReference.get(), url, new CrossActionHelper.Callback() {
|
||||||
|
@Override
|
||||||
|
public void federatedStatus(Status status) {
|
||||||
|
Intent intent = new Intent(context, ContextActivity.class);
|
||||||
|
intent.putExtra(Helper.ARG_STATUS, status);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void federatedAccount(Account account) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Helper.openBrowser(context, newURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
|
super.updateDrawState(ds);
|
||||||
|
ds.setUnderlineText(false);
|
||||||
|
ds.setColor(linkColor);
|
||||||
|
}
|
||||||
|
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- For all patterns defined in Helper class ---
|
||||||
|
for (Map.Entry<Helper.PatternType, Pattern> entry : Helper.patternHashMap.entrySet()) {
|
||||||
|
Helper.PatternType patternType = entry.getKey();
|
||||||
|
Pattern pattern = entry.getValue();
|
||||||
|
Matcher matcher = pattern.matcher(content);
|
||||||
|
while (matcher.find()) {
|
||||||
|
|
||||||
|
int matchStart = matcher.start();
|
||||||
|
int matchEnd = matcher.end();
|
||||||
|
String word = content.toString().substring(matchStart, matchEnd);
|
||||||
|
if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) {
|
||||||
|
URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class);
|
||||||
|
content.removeSpan(span);
|
||||||
|
|
||||||
|
content.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View textView) {
|
||||||
|
textView.setTag(CLICKABLE_SPAN);
|
||||||
|
switch (patternType) {
|
||||||
|
case TAG:
|
||||||
|
Intent intent = new Intent(context, HashTagActivity.class);
|
||||||
|
Bundle b = new Bundle();
|
||||||
|
b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim());
|
||||||
|
intent.putExtras(b);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
break;
|
||||||
|
case GROUP:
|
||||||
|
break;
|
||||||
|
case MENTION:
|
||||||
|
intent = new Intent(context, ProfileActivity.class);
|
||||||
|
b = new Bundle();
|
||||||
|
Mention targetedMention = null;
|
||||||
|
HashMap<String, Integer> countUsername = new HashMap<>();
|
||||||
|
for (Mention mention : announcement.mentions) {
|
||||||
|
Integer count = countUsername.get(mention.username);
|
||||||
|
if (count == null) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
if (countUsername.containsKey(mention.username)) {
|
||||||
|
countUsername.put(mention.username, count + 1);
|
||||||
|
} else {
|
||||||
|
countUsername.put(mention.username, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Mention mention : announcement.mentions) {
|
||||||
|
Integer count = countUsername.get(mention.username);
|
||||||
|
if (count == null) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) {
|
||||||
|
targetedMention = mention;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetedMention != null) {
|
||||||
|
b.putString(Helper.ARG_USER_ID, targetedMention.id);
|
||||||
|
} else {
|
||||||
|
b.putString(Helper.ARG_MENTION, word.trim());
|
||||||
|
}
|
||||||
|
intent.putExtras(b);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
break;
|
||||||
|
case MENTION_LONG:
|
||||||
|
intent = new Intent(context, ProfileActivity.class);
|
||||||
|
b = new Bundle();
|
||||||
|
targetedMention = null;
|
||||||
|
for (Mention mention : announcement.mentions) {
|
||||||
|
if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) {
|
||||||
|
targetedMention = mention;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetedMention != null) {
|
||||||
|
b.putString(Helper.ARG_USER_ID, targetedMention.id);
|
||||||
|
} else {
|
||||||
|
b.putString(Helper.ARG_MENTION, word.trim());
|
||||||
|
}
|
||||||
|
intent.putExtras(b);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
|
super.updateDrawState(ds);
|
||||||
|
ds.setUnderlineText(false);
|
||||||
|
ds.setColor(linkColor);
|
||||||
|
}
|
||||||
|
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimSpannable(new SpannableStringBuilder(content));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove extra carriage returns at the bottom due to <p> tags in toots
|
* Remove extra carriage returns at the bottom due to <p> tags in toots
|
||||||
*
|
*
|
||||||
|
@ -586,6 +1007,24 @@ public class SpannableHelper {
|
||||||
return statuses;
|
return statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static List<Announcement> convertAnnouncement(Context context, List<Announcement> announcements) {
|
||||||
|
if (announcements != null) {
|
||||||
|
for (Announcement announcement : announcements) {
|
||||||
|
convertAnnouncement(context, announcement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return announcements;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Announcement convertAnnouncement(Context context, Announcement announcement) {
|
||||||
|
if (announcement != null) {
|
||||||
|
announcement.span_content = SpannableHelper.convert(context, announcement, announcement.content);
|
||||||
|
}
|
||||||
|
return announcement;
|
||||||
|
}
|
||||||
|
|
||||||
public static Status convertStatus(Context context, Status status) {
|
public static Status convertStatus(Context context, Status status) {
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
status.span_content = SpannableHelper.convert(context, status, status.content);
|
status.span_content = SpannableHelper.convert(context, status, status.content);
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
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 static android.content.Context.INPUT_METHOD_SERVICE;
|
||||||
|
import static app.fedilab.android.BaseMainActivity.emojis;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.GridView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.vanniktech.emoji.EmojiManager;
|
||||||
|
import com.vanniktech.emoji.EmojiPopup;
|
||||||
|
import com.vanniktech.emoji.one.EmojiOneProvider;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.fedilab.android.R;
|
||||||
|
import app.fedilab.android.activities.MainActivity;
|
||||||
|
import app.fedilab.android.client.entities.api.Announcement;
|
||||||
|
import app.fedilab.android.client.entities.api.Reaction;
|
||||||
|
import app.fedilab.android.databinding.DrawerAnnouncementBinding;
|
||||||
|
import app.fedilab.android.helper.Helper;
|
||||||
|
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
|
||||||
|
|
||||||
|
|
||||||
|
public class AnnouncementAdapter extends RecyclerView.Adapter<AnnouncementAdapter.AnnouncementHolder> {
|
||||||
|
|
||||||
|
private final List<Announcement> announcements;
|
||||||
|
private Context context;
|
||||||
|
private AnnouncementsVM announcementsVM;
|
||||||
|
private AlertDialog alertDialogEmoji;
|
||||||
|
|
||||||
|
public AnnouncementAdapter(List<Announcement> announcements) {
|
||||||
|
this.announcements = announcements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return announcements.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Announcement getItem(int position) {
|
||||||
|
return announcements.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public AnnouncementHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
context = parent.getContext();
|
||||||
|
DrawerAnnouncementBinding itemBinding = DrawerAnnouncementBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
return new AnnouncementHolder(itemBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull AnnouncementHolder holder, int position) {
|
||||||
|
Announcement announcement = announcements.get(position);
|
||||||
|
if (announcement.reactions != null && announcement.reactions.size() > 0) {
|
||||||
|
ReactionAdapter reactionAdapter = new ReactionAdapter(announcement.id, announcement.reactions);
|
||||||
|
holder.binding.reactionsView.setAdapter(reactionAdapter);
|
||||||
|
LinearLayoutManager layoutManager
|
||||||
|
= new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
|
||||||
|
holder.binding.reactionsView.setLayoutManager(layoutManager);
|
||||||
|
} else {
|
||||||
|
holder.binding.reactionsView.setAdapter(null);
|
||||||
|
}
|
||||||
|
holder.binding.content.setText(announcement.span_content, TextView.BufferType.SPANNABLE);
|
||||||
|
if (announcement.starts_at != null) {
|
||||||
|
String dateIni;
|
||||||
|
String dateEnd;
|
||||||
|
if (announcement.all_day) {
|
||||||
|
dateIni = Helper.shortDateToString(announcement.starts_at);
|
||||||
|
dateEnd = Helper.shortDateToString(announcement.ends_at);
|
||||||
|
} else {
|
||||||
|
dateIni = Helper.longDateToString(announcement.starts_at);
|
||||||
|
dateEnd = Helper.longDateToString(announcement.ends_at);
|
||||||
|
}
|
||||||
|
String text = context.getString(R.string.action_announcement_from_to, dateIni, dateEnd);
|
||||||
|
holder.binding.dates.setText(text);
|
||||||
|
holder.binding.dates.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.binding.dates.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
holder.binding.statusEmoji.setOnClickListener(v -> {
|
||||||
|
EmojiManager.install(new EmojiOneProvider());
|
||||||
|
final EmojiPopup emojiPopup = EmojiPopup.Builder.fromRootView(holder.binding.statusEmoji).setOnEmojiPopupDismissListener(() -> {
|
||||||
|
InputMethodManager imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
imm.hideSoftInputFromWindow(holder.binding.statusEmoji.getWindowToken(), 0);
|
||||||
|
}).setOnEmojiClickListener((emoji, imageView) -> {
|
||||||
|
String emojiStr = imageView.getUnicode();
|
||||||
|
boolean alreadyAdded = false;
|
||||||
|
for (Reaction reaction : announcement.reactions) {
|
||||||
|
if (reaction.name.compareTo(emojiStr) == 0) {
|
||||||
|
alreadyAdded = true;
|
||||||
|
reaction.count = (reaction.count - 1);
|
||||||
|
if (reaction.count == 0) {
|
||||||
|
announcement.reactions.remove(reaction);
|
||||||
|
}
|
||||||
|
notifyItemChanged(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
Reaction reaction = new Reaction();
|
||||||
|
reaction.me = true;
|
||||||
|
reaction.count = 1;
|
||||||
|
reaction.name = emojiStr;
|
||||||
|
announcement.reactions.add(0, reaction);
|
||||||
|
notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
|
||||||
|
if (alreadyAdded) {
|
||||||
|
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
|
||||||
|
} else {
|
||||||
|
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(holder.binding.fakeEdittext);
|
||||||
|
emojiPopup.toggle();
|
||||||
|
});
|
||||||
|
holder.binding.statusAddCustomEmoji.setOnClickListener(v -> {
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
|
||||||
|
int paddingPixel = 15;
|
||||||
|
float density = context.getResources().getDisplayMetrics().density;
|
||||||
|
int paddingDp = (int) (paddingPixel * density);
|
||||||
|
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||||
|
builder.setTitle(R.string.insert_emoji);
|
||||||
|
if (emojis != null && emojis.size() > 0) {
|
||||||
|
GridView gridView = new GridView(context);
|
||||||
|
gridView.setAdapter(new EmojiAdapter(emojis));
|
||||||
|
gridView.setNumColumns(5);
|
||||||
|
gridView.setOnItemClickListener((parent, view, index, id) -> {
|
||||||
|
String emojiStr = emojis.get(index).shortcode;
|
||||||
|
String url = emojis.get(index).url;
|
||||||
|
String static_url = emojis.get(index).static_url;
|
||||||
|
boolean alreadyAdded = false;
|
||||||
|
for (Reaction reaction : announcement.reactions) {
|
||||||
|
if (reaction.name.compareTo(emojiStr) == 0) {
|
||||||
|
alreadyAdded = true;
|
||||||
|
reaction.count = (reaction.count - 1);
|
||||||
|
if (reaction.count == 0) {
|
||||||
|
announcement.reactions.remove(reaction);
|
||||||
|
}
|
||||||
|
notifyItemChanged(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
Reaction reaction = new Reaction();
|
||||||
|
reaction.me = true;
|
||||||
|
reaction.count = 1;
|
||||||
|
reaction.name = emojiStr;
|
||||||
|
reaction.url = url;
|
||||||
|
reaction.static_url = static_url;
|
||||||
|
announcement.reactions.add(0, reaction);
|
||||||
|
notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
|
||||||
|
if (alreadyAdded) {
|
||||||
|
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
|
||||||
|
} else {
|
||||||
|
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcement.id, emojiStr);
|
||||||
|
}
|
||||||
|
alertDialogEmoji.dismiss();
|
||||||
|
});
|
||||||
|
gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp);
|
||||||
|
builder.setView(gridView);
|
||||||
|
} else {
|
||||||
|
TextView textView = new TextView(context);
|
||||||
|
textView.setText(context.getString(R.string.no_emoji));
|
||||||
|
textView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp);
|
||||||
|
builder.setView(textView);
|
||||||
|
}
|
||||||
|
alertDialogEmoji = builder.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return announcements.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class AnnouncementHolder extends RecyclerView.ViewHolder {
|
||||||
|
DrawerAnnouncementBinding binding;
|
||||||
|
|
||||||
|
AnnouncementHolder(DrawerAnnouncementBinding itemView) {
|
||||||
|
super(itemView.getRoot());
|
||||||
|
binding = itemView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ package app.fedilab.android.ui.drawer;
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
|
||||||
|
import static app.fedilab.android.BaseMainActivity.emojis;
|
||||||
import static app.fedilab.android.BaseMainActivity.instanceInfo;
|
import static app.fedilab.android.BaseMainActivity.instanceInfo;
|
||||||
import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE;
|
import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE;
|
||||||
|
|
||||||
|
@ -124,7 +125,6 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||||
private final String visibility;
|
private final String visibility;
|
||||||
private final app.fedilab.android.client.entities.api.Account mentionedAccount;
|
private final app.fedilab.android.client.entities.api.Account mentionedAccount;
|
||||||
public ManageDrafts manageDrafts;
|
public ManageDrafts manageDrafts;
|
||||||
List<Emoji> emojis;
|
|
||||||
private int statusCount;
|
private int statusCount;
|
||||||
private Context context;
|
private Context context;
|
||||||
private AlertDialog alertDialogEmoji;
|
private AlertDialog alertDialogEmoji;
|
||||||
|
@ -1226,12 +1226,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||||
|
|
||||||
|
|
||||||
private void displayEmojiPicker(ComposeViewHolder holder) throws DBException {
|
private void displayEmojiPicker(ComposeViewHolder holder) throws DBException {
|
||||||
if (emojis != null) {
|
|
||||||
emojis.clear();
|
|
||||||
emojis = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance);
|
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
|
final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
|
||||||
int paddingPixel = 15;
|
int paddingPixel = 15;
|
||||||
float density = context.getResources().getDisplayMetrics().density;
|
float density = context.getResources().getDisplayMetrics().density;
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
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.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.fedilab.android.R;
|
||||||
|
import app.fedilab.android.activities.MainActivity;
|
||||||
|
import app.fedilab.android.client.entities.api.Reaction;
|
||||||
|
import app.fedilab.android.databinding.DrawerReactionBinding;
|
||||||
|
import app.fedilab.android.helper.Helper;
|
||||||
|
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Thomas on 10/03/2020.
|
||||||
|
* Adapter for reactions on messages
|
||||||
|
*/
|
||||||
|
public class ReactionAdapter extends RecyclerView.Adapter<ReactionAdapter.ReactionHolder> {
|
||||||
|
|
||||||
|
private final List<Reaction> reactions;
|
||||||
|
private final String announcementId;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
ReactionAdapter(String announcementId, List<Reaction> reactions) {
|
||||||
|
this.reactions = reactions;
|
||||||
|
this.announcementId = announcementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ReactionHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
|
||||||
|
context = parent.getContext();
|
||||||
|
DrawerReactionBinding itemBinding = DrawerReactionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
return new ReactionHolder(itemBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ReactionHolder holder, int position) {
|
||||||
|
final Reaction reaction = reactions.get(position);
|
||||||
|
|
||||||
|
holder.binding.reactionCount.setText(String.valueOf(reaction.count));
|
||||||
|
if (reaction.me) {
|
||||||
|
holder.binding.reactionContainer.setBackgroundResource(R.drawable.reaction_voted);
|
||||||
|
} else {
|
||||||
|
holder.binding.reactionContainer.setBackgroundResource(R.drawable.reaction_border);
|
||||||
|
}
|
||||||
|
if (reaction.url != null) {
|
||||||
|
holder.binding.reactionName.setVisibility(View.GONE);
|
||||||
|
holder.binding.reactionEmoji.setVisibility(View.VISIBLE);
|
||||||
|
holder.binding.reactionEmoji.setContentDescription(reaction.name);
|
||||||
|
Helper.loadImage(holder.binding.reactionEmoji, reaction.url);
|
||||||
|
} else {
|
||||||
|
holder.binding.reactionName.setText(reaction.name);
|
||||||
|
holder.binding.reactionName.setVisibility(View.VISIBLE);
|
||||||
|
holder.binding.reactionEmoji.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
AnnouncementsVM announcementsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AnnouncementsVM.class);
|
||||||
|
holder.binding.reactionContainer.setOnClickListener(v -> {
|
||||||
|
if (reaction.me) {
|
||||||
|
announcementsVM.removeReaction(MainActivity.currentInstance, MainActivity.currentToken, announcementId, reaction.name);
|
||||||
|
reaction.me = false;
|
||||||
|
} else {
|
||||||
|
announcementsVM.addReaction(MainActivity.currentInstance, MainActivity.currentToken, announcementId, reaction.name);
|
||||||
|
reaction.me = true;
|
||||||
|
}
|
||||||
|
notifyItemChanged(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return reactions.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class ReactionHolder extends RecyclerView.ViewHolder {
|
||||||
|
DrawerReactionBinding binding;
|
||||||
|
|
||||||
|
ReactionHolder(DrawerReactionBinding itemView) {
|
||||||
|
super(itemView.getRoot());
|
||||||
|
binding = itemView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package app.fedilab.android.ui.fragment.timeline;
|
||||||
|
/* 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.app.NotificationManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
public class FragmentMastodonAnnouncement extends Fragment {
|
||||||
|
|
||||||
|
|
||||||
|
private FragmentPaginationBinding binding;
|
||||||
|
private AnnouncementsVM announcementsVM;
|
||||||
|
private AnnouncementAdapter announcementAdapter;
|
||||||
|
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
binding = FragmentPaginationBinding.inflate(inflater, container, false);
|
||||||
|
View root = binding.getRoot();
|
||||||
|
binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity()));
|
||||||
|
int c1 = getResources().getColor(R.color.cyanea_accent_reference);
|
||||||
|
binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference));
|
||||||
|
binding.swipeContainer.setColorSchemeColors(
|
||||||
|
c1, c1, c1
|
||||||
|
);
|
||||||
|
announcementsVM = new ViewModelProvider(FragmentMastodonAnnouncement.this).get(AnnouncementsVM.class);
|
||||||
|
binding.loader.setVisibility(View.VISIBLE);
|
||||||
|
binding.recyclerView.setVisibility(View.GONE);
|
||||||
|
announcementsVM.getAnnouncements(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
|
||||||
|
.observe(getViewLifecycleOwner(), this::initializeAnnouncementView);
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @param announcements List of {@link Announcement}
|
||||||
|
*/
|
||||||
|
private void initializeAnnouncementView(List<Announcement> announcements) {
|
||||||
|
|
||||||
|
binding.loader.setVisibility(View.GONE);
|
||||||
|
binding.swipeContainer.setRefreshing(false);
|
||||||
|
if (announcements == null || announcements.size() == 0) {
|
||||||
|
binding.noActionText.setText(R.string.no_announcements);
|
||||||
|
binding.noAction.setVisibility(View.VISIBLE);
|
||||||
|
binding.recyclerView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
binding.noAction.setVisibility(View.GONE);
|
||||||
|
binding.recyclerView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
announcementAdapter = new AnnouncementAdapter(announcements);
|
||||||
|
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
|
||||||
|
binding.recyclerView.setLayoutManager(mLayoutManager);
|
||||||
|
binding.recyclerView.setAdapter(announcementAdapter);
|
||||||
|
|
||||||
|
|
||||||
|
binding.swipeContainer.setOnRefreshListener(() -> {
|
||||||
|
binding.swipeContainer.setRefreshing(true);
|
||||||
|
announcementsVM.getAnnouncements(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
|
||||||
|
.observe(getViewLifecycleOwner(), this::initializeAnnouncementView);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollToTop() {
|
||||||
|
binding.recyclerView.scrollToPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
binding.recyclerView.setAdapter(null);
|
||||||
|
announcementAdapter = null;
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ import app.fedilab.android.helper.SpannableHelper;
|
||||||
import app.fedilab.android.helper.ThemeHelper;
|
import app.fedilab.android.helper.ThemeHelper;
|
||||||
import app.fedilab.android.ui.drawer.StatusAdapter;
|
import app.fedilab.android.ui.drawer.StatusAdapter;
|
||||||
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
|
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
|
||||||
|
import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM;
|
||||||
import app.fedilab.android.viewmodel.mastodon.SearchVM;
|
import app.fedilab.android.viewmodel.mastodon.SearchVM;
|
||||||
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
|
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
|
||||||
|
|
||||||
|
@ -70,6 +71,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
||||||
private FragmentPaginationBinding binding;
|
private FragmentPaginationBinding binding;
|
||||||
private TimelinesVM timelinesVM;
|
private TimelinesVM timelinesVM;
|
||||||
private AccountsVM accountsVM;
|
private AccountsVM accountsVM;
|
||||||
|
private AnnouncementsVM announcementsVM;
|
||||||
private boolean flagLoading;
|
private boolean flagLoading;
|
||||||
private List<Status> statuses;
|
private List<Status> statuses;
|
||||||
private String search, searchCache;
|
private String search, searchCache;
|
||||||
|
@ -237,6 +239,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
||||||
|
|
||||||
timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class);
|
timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class);
|
||||||
accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class);
|
accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class);
|
||||||
|
announcementsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AnnouncementsVM.class);
|
||||||
|
|
||||||
binding.loader.setVisibility(View.VISIBLE);
|
binding.loader.setVisibility(View.VISIBLE);
|
||||||
binding.recyclerView.setVisibility(View.GONE);
|
binding.recyclerView.setVisibility(View.GONE);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import app.fedilab.android.client.endpoints.MastodonAnnouncementsService;
|
import app.fedilab.android.client.endpoints.MastodonAnnouncementsService;
|
||||||
import app.fedilab.android.client.entities.api.Announcement;
|
import app.fedilab.android.client.entities.api.Announcement;
|
||||||
import app.fedilab.android.helper.Helper;
|
import app.fedilab.android.helper.Helper;
|
||||||
|
import app.fedilab.android.helper.SpannableHelper;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
@ -67,7 +68,7 @@ public class AnnouncementsVM extends AndroidViewModel {
|
||||||
* @param withDismissed If true, response will include announcements dismissed by the user. Defaults to false.
|
* @param withDismissed If true, response will include announcements dismissed by the user. Defaults to false.
|
||||||
* @return {@link LiveData} containing a {@link List} of {@link Announcement}s
|
* @return {@link LiveData} containing a {@link List} of {@link Announcement}s
|
||||||
*/
|
*/
|
||||||
public LiveData<List<Announcement>> getAnnouncements(@NonNull String instance, String token, boolean withDismissed) {
|
public LiveData<List<Announcement>> getAnnouncements(@NonNull String instance, String token, Boolean withDismissed) {
|
||||||
MastodonAnnouncementsService mastodonAnnouncementsService = init(instance);
|
MastodonAnnouncementsService mastodonAnnouncementsService = init(instance);
|
||||||
announcementListMutableLiveData = new MutableLiveData<>();
|
announcementListMutableLiveData = new MutableLiveData<>();
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
|
@ -78,6 +79,7 @@ public class AnnouncementsVM extends AndroidViewModel {
|
||||||
Response<List<Announcement>> getAnnouncementsResponse = getAnnouncementsCall.execute();
|
Response<List<Announcement>> getAnnouncementsResponse = getAnnouncementsCall.execute();
|
||||||
if (getAnnouncementsResponse.isSuccessful()) {
|
if (getAnnouncementsResponse.isSuccessful()) {
|
||||||
announcementList = getAnnouncementsResponse.body();
|
announcementList = getAnnouncementsResponse.body();
|
||||||
|
SpannableHelper.convertAnnouncement(getApplication(), announcementList);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
10
app/src/main/res/drawable/ic_baseline_add_reaction_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_add_reaction_24.xml
Normal file
|
@ -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="M18,9V7h-2V2.84C14.77,2.3 13.42,2 11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12c0,-1.05 -0.17,-2.05 -0.47,-3H18zM15.5,8C16.33,8 17,8.67 17,9.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5zM22,3h2v2h-2v2h-2V5h-2V3h2V1h2V3z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml
Normal file
|
@ -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="M11.99,2C6.47,2 2,6.48 2,12c0,5.52 4.47,10 9.99,10C17.52,22 22,17.52 22,12C22,6.48 17.52,2 11.99,2zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,18c-2.28,0 -4.22,-1.66 -5,-4h10C16.22,16.34 14.28,18 12,18zM15.5,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S14.67,8 15.5,8S17,8.67 17,9.5S16.33,11 15.5,11z" />
|
||||||
|
</vector>
|
|
@ -2,7 +2,7 @@
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:autoMirrored="true"
|
android:autoMirrored="true"
|
||||||
android:tint="?attr/colorControlNormal"
|
android:tint="#FFFFFF"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
|
|
7
app/src/main/res/drawable/reaction_border.xml
Normal file
7
app/src/main/res/drawable/reaction_border.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="?colorAccent" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
8
app/src/main/res/drawable/reaction_voted.xml
Normal file
8
app/src/main/res/drawable/reaction_voted.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?colorAccent" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="?colorAccent" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
62
app/src/main/res/layout/activity_announcement.xml
Normal file
62
app/src/main/res/layout/activity_announcement.xml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?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>
|
||||||
|
-->
|
||||||
|
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/drawer_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/cyanea_primary_reference">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
style="@style/TextAppearance.AppCompat.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical" />
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/nav_host_fragment_tags"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
</androidx.drawerlayout.widget.DrawerLayout>
|
121
app/src/main/res/layout/drawer_announcement.xml
Normal file
121
app/src/main/res/layout/drawer_announcement.xml
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<?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>
|
||||||
|
-->
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/cardview_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/card_margin"
|
||||||
|
android:layout_marginTop="@dimen/card_margin"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/card_status_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/card_margin"
|
||||||
|
android:layout_marginTop="@dimen/card_margin"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/dates"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<app.fedilab.android.helper.CustomTextView
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/dates"
|
||||||
|
tools:maxLines="10"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:id="@+id/status_reactions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/content">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/reactions_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/status_add_custom_emoji"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:contentDescription="@string/add_reaction"
|
||||||
|
android:src="@drawable/ic_baseline_emoji_emotions_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?attr/iconColor" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/status_emoji"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:contentDescription="@string/add_reaction"
|
||||||
|
android:src="@drawable/ic_baseline_add_reaction_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?attr/iconColor" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<app.fedilab.android.helper.FedilabAutoCompleteTextView
|
||||||
|
android:id="@+id/fake_edittext"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:importantForAutofill="noExcludeDescendants"
|
||||||
|
android:inputType="text" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
52
app/src/main/res/layout/drawer_reaction.xml
Normal file
52
app/src/main/res/layout/drawer_reaction.xml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?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>.
|
||||||
|
-->
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/reaction_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="5dp"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingRight="5dp"
|
||||||
|
android:paddingBottom="2dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/reaction_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginEnd="3dp"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/reaction_emoji"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginEnd="3dp"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/reaction_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
|
@ -28,6 +28,13 @@
|
||||||
android:id="@+id/nav_list"
|
android:id="@+id/nav_list"
|
||||||
android:icon="@drawable/ic_baseline_view_list_24"
|
android:icon="@drawable/ic_baseline_view_list_24"
|
||||||
android:title="@string/action_lists" />
|
android:title="@string/action_lists" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/nav_announcements"
|
||||||
|
android:icon="@drawable/ic_baseline_message_24"
|
||||||
|
android:title="@string/action_announcements"
|
||||||
|
android:visible="true" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_scheduled"
|
android:id="@+id/nav_scheduled"
|
||||||
android:icon="@drawable/ic_baseline_schedule_24"
|
android:icon="@drawable/ic_baseline_schedule_24"
|
||||||
|
|
|
@ -1626,6 +1626,7 @@
|
||||||
<string name="msg_save_image">Are you want to exit without saving image ?</string>
|
<string name="msg_save_image">Are you want to exit without saving image ?</string>
|
||||||
<string name="msg_share_image">Share Image</string>
|
<string name="msg_share_image">Share Image</string>
|
||||||
<string name="tap_here_to_refresh_poll">Tap here to refresh poll</string>
|
<string name="tap_here_to_refresh_poll">Tap here to refresh poll</string>
|
||||||
|
<string name="action_announcement_from_to">Announcement · %1$s - %2$s</string>
|
||||||
|
|
||||||
<string-array name="photo_editor_emoji" translatable="false">
|
<string-array name="photo_editor_emoji" translatable="false">
|
||||||
<!-- Smiles -->
|
<!-- Smiles -->
|
||||||
|
|
Loading…
Reference in a new issue