feat: implement notification badge using markers from @sk22.
Thank you a lot man! This improvement is amazing
This commit is contained in:
parent
9d60924512
commit
5395855775
|
@ -24,6 +24,7 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.NotificationAction;
|
import org.joinmastodon.android.model.NotificationAction;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
|
@ -84,6 +85,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||||
}
|
}
|
||||||
String accountID=account.getID();
|
String accountID=account.getID();
|
||||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||||
|
E.post(new NotificationReceivedEvent(pn.notificationId+""));
|
||||||
new GetNotificationByID(pn.notificationId+"")
|
new GetNotificationByID(pn.notificationId+"")
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -11,7 +11,7 @@ public class ApiUtils{
|
||||||
//no instance
|
//no instance
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){
|
public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){
|
||||||
return e.stream().map(ev->{
|
return e.stream().map(ev->{
|
||||||
try{
|
try{
|
||||||
SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class);
|
SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class);
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.joinmastodon.android.api.requests.markers;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ApiUtils;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Marker;
|
||||||
|
import org.joinmastodon.android.model.Markers;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
public class GetMarkers extends MastodonAPIRequest<Markers> {
|
||||||
|
public GetMarkers(EnumSet<Marker.Type> timelines) {
|
||||||
|
super(HttpMethod.GET, "/markers", Markers.class);
|
||||||
|
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
|
||||||
|
addQueryParameter("timeline[]", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Application;
|
import org.joinmastodon.android.model.Application;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Markers;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
@ -31,6 +32,7 @@ public class AccountSession{
|
||||||
public String pushAccountID;
|
public String pushAccountID;
|
||||||
public Preferences preferences;
|
public Preferences preferences;
|
||||||
public AccountActivationInfo activationInfo;
|
public AccountActivationInfo activationInfo;
|
||||||
|
public Markers markers;
|
||||||
private transient MastodonAPIController apiController;
|
private transient MastodonAPIController apiController;
|
||||||
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
|
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
|
||||||
private transient CacheController cacheController;
|
private transient CacheController cacheController;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||||
|
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
@ -33,6 +34,8 @@ import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.EmojiCategory;
|
import org.joinmastodon.android.model.EmojiCategory;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.Marker;
|
||||||
|
import org.joinmastodon.android.model.Markers;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
|
@ -46,6 +49,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -255,6 +259,7 @@ public class AccountSessionManager{
|
||||||
// if(now-session.filtersLastUpdated>3600_000L){
|
// if(now-session.filtersLastUpdated>3600_000L){
|
||||||
updateSessionWordFilters(session);
|
updateSessionWordFilters(session);
|
||||||
// }
|
// }
|
||||||
|
updateSessionMarkers(session);
|
||||||
}
|
}
|
||||||
if(loadedInstances){
|
if(loadedInstances){
|
||||||
maybeUpdateCustomEmojis(domains);
|
maybeUpdateCustomEmojis(domains);
|
||||||
|
@ -319,6 +324,21 @@ public class AccountSessionManager{
|
||||||
.exec(session.getID());
|
.exec(session.getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateSessionMarkers(AccountSession session) {
|
||||||
|
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Markers markers) {
|
||||||
|
session.markers = markers;
|
||||||
|
writeAccountsFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}).exec(session.getID());
|
||||||
|
}
|
||||||
|
|
||||||
public void updateInstanceInfo(String domain){
|
public void updateInstanceInfo(String domain){
|
||||||
new GetInstance()
|
new GetInstance()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class AllNotificationsSeenEvent {
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class NotificationReceivedEvent {
|
||||||
|
public String id;
|
||||||
|
public NotificationReceivedEvent(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,19 +23,34 @@ import androidx.annotation.Nullable;
|
||||||
import org.joinmastodon.android.DomainManager;
|
import org.joinmastodon.android.DomainManager;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.MainActivity;
|
import org.joinmastodon.android.MainActivity;
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||||
|
import org.joinmastodon.android.events.NotificationReceivedEvent;
|
||||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.TabBar;
|
import org.joinmastodon.android.ui.views.TabBar;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.IdRes;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.fragments.AppKitFragment;
|
import me.grishka.appkit.fragments.AppKitFragment;
|
||||||
import me.grishka.appkit.fragments.LoaderFragment;
|
import me.grishka.appkit.fragments.LoaderFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
|
@ -58,7 +73,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
private View tabBarWrap;
|
private View tabBarWrap;
|
||||||
private ImageView tabBarAvatar;
|
private ImageView tabBarAvatar;
|
||||||
private ImageView notificationTabIcon;
|
private ImageView notificationTabIcon;
|
||||||
private boolean notificationBadged = false;
|
|
||||||
@IdRes
|
@IdRes
|
||||||
private int currentTab=R.id.tab_home;
|
private int currentTab=R.id.tab_home;
|
||||||
|
|
||||||
|
@ -67,6 +81,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
E.register(this);
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
setTitle(R.string.mo_app_name);
|
setTitle(R.string.mo_app_name);
|
||||||
|
|
||||||
|
@ -124,6 +139,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||||
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
|
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
|
||||||
|
|
||||||
|
notificationTabIcon=content.findViewById(R.id.tab_notifications);
|
||||||
|
updateNotificationBadge();
|
||||||
|
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.add(R.id.fragment_wrap, homeTabFragment)
|
.add(R.id.fragment_wrap, homeTabFragment)
|
||||||
|
@ -344,4 +362,49 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
// getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
// getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||||
// getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
// getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateNotificationBadge() {
|
||||||
|
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||||
|
|
||||||
|
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
|
||||||
|
.setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Notification> notifications) {
|
||||||
|
if (notifications.size() > 0) {
|
||||||
|
try {
|
||||||
|
long newestId = Long.parseLong(notifications.get(0).id);
|
||||||
|
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
|
||||||
|
System.out.println("NEWEST: " + newestId);
|
||||||
|
System.out.println("LAST SEEN: " + lastSeenId);
|
||||||
|
|
||||||
|
setNotificationBadge(newestId > lastSeenId);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
setNotificationBadge(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
setNotificationBadge(false);
|
||||||
|
}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotificationBadge(boolean badge) {
|
||||||
|
notificationTabIcon.setImageResource(badge
|
||||||
|
? R.drawable.ic_fluent_alert_28_selector_badged
|
||||||
|
: R.drawable.ic_fluent_alert_28_selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) {
|
||||||
|
setNotificationBadge(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
|
||||||
|
setNotificationBadge(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
@ -145,7 +146,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
maxID=result.maxID;
|
maxID=result.maxID;
|
||||||
|
|
||||||
if(offset==0 && !result.items.isEmpty()){
|
if(offset==0 && !result.items.isEmpty()){
|
||||||
|
E.post(new AllNotificationsSeenEvent());
|
||||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).markers
|
||||||
|
.notifications.lastReadId = result.items.get(0).id;
|
||||||
|
AccountSessionManager.getInstance().writeAccountsFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -18,4 +20,11 @@ public class Marker extends BaseModel{
|
||||||
", updatedAt="+updatedAt+
|
", updatedAt="+updatedAt+
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
@SerializedName("home")
|
||||||
|
HOME,
|
||||||
|
@SerializedName("notifications")
|
||||||
|
NOTIFICATIONS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
public class Markers {
|
||||||
|
public Marker notifications;
|
||||||
|
public Marker home;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Markers{" +
|
||||||
|
"notifications=" + notifications +
|
||||||
|
", home=" + home +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_filled" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<stroke android:color="?toolbarBackground" android:width="2dp"/>
|
||||||
|
<solid android:color="?android:colorAccent"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<stroke android:color="?toolbarBackground" android:width="2dp"/>
|
||||||
|
<solid android:color="?android:colorAccent"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_activated="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_checked="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_filled_badged" android:state_selected="true"/>
|
||||||
|
<item android:drawable="@drawable/ic_fluent_alert_28_regular_badged"/>
|
||||||
|
</selector>
|
|
@ -51,7 +51,6 @@
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:contentDescription="@string/notifications"
|
android:contentDescription="@string/notifications"
|
||||||
android:background="?android:selectableItemBackgroundBorderless"
|
android:background="?android:selectableItemBackgroundBorderless"
|
||||||
android:tint="?android:colorPrimary"
|
|
||||||
android:src="@drawable/ic_fluent_alert_28_selector"/>
|
android:src="@drawable/ic_fluent_alert_28_selector"/>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
|
|
Loading…
Reference in New Issue