Fitur notifikasi dan foreground
Add: - types/type-notification-category.ts Fix: - app/(application)/(user)/notifications/index.tsx - app/(application)/(user)/test-notifications.tsx - app/(application)/admin/notification/index.tsx - components/Notification/NotificationInitializer.tsx - hooks/use-notification-store.tsx - service/api-notifications.ts - utils/formatChatTime.ts ### No Issue
This commit is contained in:
@@ -1,79 +1,57 @@
|
|||||||
import {
|
import {
|
||||||
BaseBox,
|
BaseBox,
|
||||||
Grid,
|
NewWrapper,
|
||||||
ScrollableCustom,
|
ScrollableCustom,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom
|
||||||
ViewWrapper,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { MainColor } from "@/constants/color-palet";
|
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||||
import { useState } from "react";
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
import { View } from "react-native";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
const categories = [
|
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||||
{ value: "all", label: "Semua" },
|
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||||
{ value: "event", label: "Event" },
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
{ value: "job", label: "Job" },
|
import { router, useFocusEffect } from "expo-router";
|
||||||
{ value: "voting", label: "Voting" },
|
import { useCallback, useState } from "react";
|
||||||
{ value: "donasi", label: "Donasi" },
|
import { RefreshControl, View } from "react-native";
|
||||||
{ value: "investasi", label: "Investasi" },
|
|
||||||
{ value: "forum", label: "Forum" },
|
|
||||||
{ value: "collaboration", label: "Collaboration" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedCategory = (value: string) => {
|
const selectedCategory = (value: string) => {
|
||||||
const category = categories.find((c) => c.value === value);
|
const category = listOfcategoriesAppNotification.find((c) => c.value === value);
|
||||||
return category?.label;
|
return category?.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoxNotification = ({
|
const BoxNotification = ({
|
||||||
index,
|
data,
|
||||||
activeCategory,
|
activeCategory,
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
data: any;
|
||||||
activeCategory: string | null;
|
activeCategory: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { markAsRead } = useNotificationStore();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseBox
|
<BaseBox
|
||||||
onPress={() =>
|
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||||
|
onPress={() => {
|
||||||
console.log(
|
console.log(
|
||||||
"Notification >",
|
"Notification >",
|
||||||
selectedCategory(activeCategory as string)
|
selectedCategory(activeCategory as string)
|
||||||
)
|
);
|
||||||
}
|
router.push(data.deepLink);
|
||||||
|
markAsRead(data.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<StackCustom>
|
<StackCustom>
|
||||||
<TextCustom bold>
|
<TextCustom truncate={2} bold>
|
||||||
# {selectedCategory(activeCategory as string)}
|
{data.title}
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
|
|
||||||
<View
|
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||||
style={{
|
|
||||||
borderBottomColor: MainColor.white_gray,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextCustom truncate={2}>
|
<TextCustom size="small" color="gray">
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint odio
|
{formatChatTime(data.createdAt)}
|
||||||
unde quidem voluptate quam culpa sequi molestias ipsa corrupti id,
|
|
||||||
soluta, nostrum adipisci similique, et illo asperiores deleniti eum
|
|
||||||
labore.
|
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<TextCustom size="small" color="gray">
|
|
||||||
{index + 1} Agustus 2025
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
|
|
||||||
<TextCustom size="small" color="gray">
|
|
||||||
Belum lihat
|
|
||||||
</TextCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
</>
|
</>
|
||||||
@@ -81,17 +59,54 @@ const BoxNotification = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||||
|
const [listData, setListData] = useState<any[]>([]);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handlePress = (item: any) => {
|
const handlePress = (item: any) => {
|
||||||
setActiveCategory(item.value);
|
setActiveCategory(item.value);
|
||||||
// tambahkan logika lain seperti filter dsb.
|
// tambahkan logika lain seperti filter dsb.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fecthData();
|
||||||
|
}, [activeCategory])
|
||||||
|
);
|
||||||
|
|
||||||
|
const fecthData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiGetNotificationsById({
|
||||||
|
id: user?.id as any,
|
||||||
|
category: activeCategory as any,
|
||||||
|
});
|
||||||
|
// console.log("Response Notification", JSON.stringify(response, null, 2));
|
||||||
|
if (response.success) {
|
||||||
|
setListData(response.data);
|
||||||
|
} else {
|
||||||
|
setListData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error Notification", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
fecthData();
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewWrapper
|
<NewWrapper
|
||||||
headerComponent={
|
headerComponent={
|
||||||
<ScrollableCustom
|
<ScrollableCustom
|
||||||
data={categories.map((e, i) => ({
|
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||||
id: i,
|
id: i,
|
||||||
label: e.label,
|
label: e.label,
|
||||||
value: e.value,
|
value: e.value,
|
||||||
@@ -100,12 +115,19 @@ export default function Notifications() {
|
|||||||
activeId={activeCategory as string}
|
activeId={activeCategory as string}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 20 }).map((e, i) => (
|
{loading ? (
|
||||||
<View key={i}>
|
<ListSkeletonComponent/>
|
||||||
<BoxNotification index={i} activeCategory={activeCategory as any} />
|
) : (
|
||||||
</View>
|
listData.map((e, i) => (
|
||||||
))}
|
<View key={i}>
|
||||||
</ViewWrapper>
|
<BoxNotification data={e} activeCategory={activeCategory as any} />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</NewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ export default function TestNotification() {
|
|||||||
console.log("[Data Dikirim]", data);
|
console.log("[Data Dikirim]", data);
|
||||||
const response = await apiNotificationsSend({
|
const response = await apiNotificationsSend({
|
||||||
data: {
|
data: {
|
||||||
title: "Test dari Backend (App Router)!",
|
title: "Test Notification !!",
|
||||||
body: data,
|
body: data,
|
||||||
userLoginId: user?.id || "",
|
userLoginId: user?.id || "",
|
||||||
appId: "hipmi",
|
appId: "hipmi",
|
||||||
status: "publish",
|
status: "publish",
|
||||||
kategoriApp: "EVENT",
|
kategoriApp: "JOB",
|
||||||
type: "announcement",
|
type: "announcement",
|
||||||
deepLink: "event/23189913801",
|
deepLink: "/job/cmhjz8u3h0005cfaxezyeilrr",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,103 @@
|
|||||||
import { BackButton, TextCustom, ViewWrapper } from "@/components";
|
import {
|
||||||
import { Stack } from "expo-router";
|
BackButton,
|
||||||
|
BaseBox,
|
||||||
|
NewWrapper,
|
||||||
|
ScrollableCustom,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import { AccentColor } from "@/constants/color-palet";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||||
|
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||||
|
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||||
|
import { formatChatTime } from "@/utils/formatChatTime";
|
||||||
|
import { router, Stack, useFocusEffect } from "expo-router";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { RefreshControl, View } from "react-native";
|
||||||
|
|
||||||
|
const selectedCategory = (value: string) => {
|
||||||
|
const category = listOfcategoriesAppNotification.find((c) => c.value === value);
|
||||||
|
return category?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoxNotification = ({
|
||||||
|
data,
|
||||||
|
activeCategory,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
activeCategory: string | null;
|
||||||
|
}) => {
|
||||||
|
const { markAsRead } = useNotificationStore();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseBox
|
||||||
|
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||||
|
onPress={() => {
|
||||||
|
console.log(
|
||||||
|
"Notification >",
|
||||||
|
selectedCategory(activeCategory as string)
|
||||||
|
);
|
||||||
|
router.push(data.deepLink);
|
||||||
|
markAsRead(data.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StackCustom>
|
||||||
|
<TextCustom truncate={2} bold>
|
||||||
|
{data.title}
|
||||||
|
</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||||
|
|
||||||
|
<TextCustom size="small" color="gray">
|
||||||
|
{formatChatTime(data.createdAt)}
|
||||||
|
</TextCustom>
|
||||||
|
</StackCustom>
|
||||||
|
</BaseBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdminNotification() {
|
export default function AdminNotification() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||||
|
const [listData, setListData] = useState<any[]>([]);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const handlePress = (item: any) => {
|
||||||
|
setActiveCategory(item.value);
|
||||||
|
// tambahkan logika lain seperti filter dsb.
|
||||||
|
};
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fecthData();
|
||||||
|
}, [activeCategory])
|
||||||
|
);
|
||||||
|
|
||||||
|
const fecthData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiGetNotificationsById({
|
||||||
|
id: user?.id as any,
|
||||||
|
category: activeCategory as any,
|
||||||
|
});
|
||||||
|
// console.log("Response Notification", JSON.stringify(response, null, 2));
|
||||||
|
if (response.success) {
|
||||||
|
setListData(response.data);
|
||||||
|
} else {
|
||||||
|
setListData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error Notification", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
fecthData();
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -12,9 +108,28 @@ export default function AdminNotification() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewWrapper>
|
<NewWrapper
|
||||||
<TextCustom>Notification</TextCustom>
|
headerComponent={
|
||||||
</ViewWrapper>
|
<ScrollableCustom
|
||||||
|
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||||
|
id: i,
|
||||||
|
label: e.label,
|
||||||
|
value: e.value,
|
||||||
|
}))}
|
||||||
|
onButtonPress={handlePress}
|
||||||
|
activeId={activeCategory as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{listData.map((e, i) => (
|
||||||
|
<View key={i}>
|
||||||
|
<BoxNotification data={e} activeCategory={activeCategory as any} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</NewWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default function NotificationInitializer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("📥 Menambahkan ke store:", { title, body, safeData });
|
console.log("📥 Menambahkan ke store:", { title, body, safeData });
|
||||||
addNotification({ title, body, data: safeData, type: "notification" });
|
addNotification({ title, body, data: safeData, type: "announcement" });
|
||||||
console.log("✅ Notifikasi ditambahkan ke state");
|
console.log("✅ Notifikasi ditambahkan ke state");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
// hooks/useNotificationStore.ts
|
// hooks/useNotificationStore.ts
|
||||||
|
import {
|
||||||
|
apiNotificationMarkAsRead,
|
||||||
|
apiNotificationUnreadCount,
|
||||||
|
} from "@/service/api-notifications";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@@ -7,10 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useAuth } from "./use-auth";
|
import { useAuth } from "./use-auth";
|
||||||
import {
|
|
||||||
apiGetNotificationsById,
|
|
||||||
apiNotificationUnreadCount,
|
|
||||||
} from "@/service/api-notifications";
|
|
||||||
|
|
||||||
type AppNotification = {
|
type AppNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,7 +19,7 @@ type AppNotification = {
|
|||||||
data?: Record<string, string>;
|
data?: Record<string, string>;
|
||||||
isRead: boolean;
|
isRead: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
type: "notification" | "trigger";
|
type: "announcement" | "trigger";
|
||||||
// untuk id dari setiap kategori app
|
// untuk id dari setiap kategori app
|
||||||
appId?: string;
|
appId?: string;
|
||||||
kategoriApp?:
|
kategoriApp?:
|
||||||
@@ -70,7 +70,7 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
try {
|
try {
|
||||||
const count = await apiNotificationUnreadCount({
|
const count = await apiNotificationUnreadCount({
|
||||||
id: user?.id as any,
|
id: user?.id as any,
|
||||||
role: user?.masterUserRoleId as any
|
role: user?.masterUserRoleId as any,
|
||||||
}); // ← harus return number
|
}); // ← harus return number
|
||||||
const result = count.data;
|
const result = count.data;
|
||||||
console.log("📖 Unread count:", result);
|
console.log("📖 Unread count:", result);
|
||||||
@@ -96,10 +96,22 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
setUnreadCount((prev) => prev + 1);
|
setUnreadCount((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAsRead = (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
setNotifications((prev) =>
|
try {
|
||||||
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
const response = await apiNotificationMarkAsRead({ id });
|
||||||
);
|
console.log("🚀 Response Mark As Read:", response);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const cloneNotifications = [...notifications];
|
||||||
|
const index = cloneNotifications.findIndex((n) => n?.data?.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
cloneNotifications[index].isRead = true;
|
||||||
|
setNotifications(cloneNotifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal mark as read:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncUnreadCount = async () => {
|
const syncUnreadCount = async () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TypeNotificationCategoryApp } from "@/types/type-notification-category";
|
||||||
import { apiConfig } from "./api-config";
|
import { apiConfig } from "./api-config";
|
||||||
|
|
||||||
type NotificationProp = {
|
type NotificationProp = {
|
||||||
@@ -8,15 +9,7 @@ type NotificationProp = {
|
|||||||
status?: string;
|
status?: string;
|
||||||
type?: "announcement" | "trigger";
|
type?: "announcement" | "trigger";
|
||||||
deepLink?: string;
|
deepLink?: string;
|
||||||
kategoriApp?:
|
kategoriApp?: TypeNotificationCategoryApp
|
||||||
| "JOB"
|
|
||||||
| "VOTING"
|
|
||||||
| "EVENT"
|
|
||||||
| "DONASI"
|
|
||||||
| "INVESTASI"
|
|
||||||
| "COLLABORATION"
|
|
||||||
| "FORUM"
|
|
||||||
| "ACCESS"; // Untuk trigger akses user;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function apiNotificationsSend({
|
export async function apiNotificationsSend({
|
||||||
@@ -40,7 +33,7 @@ export async function apiGetNotificationsById({
|
|||||||
category,
|
category,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
category: "count-as-unread" | "all";
|
category: TypeNotificationCategoryApp
|
||||||
}) {
|
}) {
|
||||||
console.log("ID", id);
|
console.log("ID", id);
|
||||||
console.log("Category", category);
|
console.log("Category", category);
|
||||||
@@ -68,3 +61,13 @@ export async function apiNotificationUnreadCount({ id, role }: { id: string, rol
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function apiNotificationMarkAsRead({id}: {id: string}) {
|
||||||
|
try {
|
||||||
|
const response = await apiConfig.put(`/mobile/notification/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
types/type-notification-category.ts
Normal file
19
types/type-notification-category.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type TypeNotificationCategoryApp =
|
||||||
|
| "EVENT"
|
||||||
|
| "JOB"
|
||||||
|
| "VOTING"
|
||||||
|
| "DONASI"
|
||||||
|
| "INVESTASI"
|
||||||
|
| "COLLABORATION"
|
||||||
|
| "FORUM"
|
||||||
|
| "ACCESS";
|
||||||
|
|
||||||
|
export const listOfcategoriesAppNotification = [
|
||||||
|
{ value: "event", label: "Event" },
|
||||||
|
{ value: "job", label: "Job" },
|
||||||
|
{ value: "voting", label: "Voting" },
|
||||||
|
{ value: "donasi", label: "Donasi" },
|
||||||
|
{ value: "investasi", label: "Investasi" },
|
||||||
|
{ value: "forum", label: "Forum" },
|
||||||
|
{ value: "collaboration", label: "Collaboration" },
|
||||||
|
];
|
||||||
@@ -17,7 +17,7 @@ export const formatChatTime = (date: string | Date): string => {
|
|||||||
|
|
||||||
// Jika hari ini
|
// Jika hari ini
|
||||||
if (messageDate.isSame(now, 'day')) {
|
if (messageDate.isSame(now, 'day')) {
|
||||||
return messageDate.format('HH.mm'); // contoh: "14.30"
|
return messageDate.format('HH:mm'); // contoh: "14.30"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika kemarin
|
// Jika kemarin
|
||||||
@@ -31,5 +31,5 @@ export const formatChatTime = (date: string | Date): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lebih dari seminggu lalu → tampilkan tanggal
|
// Lebih dari seminggu lalu → tampilkan tanggal
|
||||||
return messageDate.format('D MMM YYYY'); // contoh: "12 Mei 2024"
|
return messageDate.format('D MMM YYYY HH:mm'); // contoh: "12 Mei 2024 14:30"
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user