mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-11-21 01:21:54 +08:00
Improve notification dropdown
This commit is contained in:
parent
3db198214d
commit
09a4d1072e
@ -20,6 +20,9 @@ import IconMdiKey from "~icons/mdi/key";
|
||||
import IconMdiFileCodumentAlert from "~icons/mdi/file-document-alert";
|
||||
import IconMdiBellOutline from "~icons/mdi/bell-outline";
|
||||
import IconMdiBellBadge from "~icons/mdi/bell-badge";
|
||||
import IconMdiAlertOutline from "~icons/mdi/alert-outline";
|
||||
import IconMdiInformationOutline from "~icons/mdi/information-outline";
|
||||
import IconMdiCheck from "~icons/mdi/check";
|
||||
|
||||
import { useAuthStore } from "~/store/auth";
|
||||
import { useAuth } from "~/composables/useAuth";
|
||||
@ -33,8 +36,9 @@ import { useNotificationsAmount } from "~/composables/useApiHelper";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { HangarNotification } from "hangar-internal";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { Ref, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import Link from "~/components/design/Link.vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
@ -42,11 +46,26 @@ const backendData = useBackendDataStore();
|
||||
|
||||
const ctx = useContext();
|
||||
const i18n = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const notifications = ref<HangarNotification[]>([]);
|
||||
useNotificationsAmount(true, 10)
|
||||
.then((v) => (notifications.value = filteredNotifications(v as Ref<HangarNotification[]>)))
|
||||
.catch((e) => handleRequestError(e, ctx, i18n));
|
||||
const unreadNotifications = ref<boolean>(false);
|
||||
if (authStore.user) {
|
||||
useNotificationsAmount(true, 10)
|
||||
.then((v) => {
|
||||
if (v && v.value) {
|
||||
//TODO filter recent notifications
|
||||
notifications.value = v.value;
|
||||
}
|
||||
|
||||
for (const notification of notifications.value) {
|
||||
if (!notification.read) {
|
||||
unreadNotifications.value = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => handleRequestError(e, ctx, i18n));
|
||||
}
|
||||
|
||||
const navBarLinks = [
|
||||
{ link: "index", label: "Home" },
|
||||
@ -73,15 +92,18 @@ const navBarMenuLinksMoreFromPaper = [
|
||||
{ link: "https://hangar-auth.benndorf.dev/", label: t("nav.hangar.auth"), icon: IconMdiKey },
|
||||
];
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const auth = useAuth;
|
||||
const authHost = import.meta.env.HANGAR_AUTH_HOST;
|
||||
authLog("render with user " + authStore.user?.name);
|
||||
|
||||
function filteredNotifications(notificationsRef: Ref<HangarNotification[]>): HangarNotification[] {
|
||||
if (!notificationsRef || !notificationsRef.value) return [];
|
||||
//TODO filter recent notifications
|
||||
return notificationsRef.value;
|
||||
async function markNotificationRead() {
|
||||
for (const notification of notifications.value) {
|
||||
if (!notification.read) {
|
||||
useInternalApi(`notifications/${notification.id}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n));
|
||||
notification.read = true;
|
||||
}
|
||||
}
|
||||
unreadNotifications.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -173,20 +195,36 @@ function filteredNotifications(notificationsRef: Ref<HangarNotification[]>): Han
|
||||
<icon-mdi-white-balance-sunny v-else class="text-[1.2em]"></icon-mdi-white-balance-sunny>
|
||||
</button>
|
||||
<div v-if="authStore.user">
|
||||
<!-- todo: make prettier -->
|
||||
<!-- todo: either mark as read when opened and then closed, or have a "Mark as read" action -->
|
||||
<!-- todo: make prettier (show all unread and not just recent 20 unread/read, actually use action field) -->
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<div class="flex items-center gap-2 rounded-md p-2" hover="text-primary-400 bg-primary-0">
|
||||
<IconMdiBellOutline v-if="notifications.length === 0" class="text-[1.2em]" />
|
||||
<IconMdiBellBadge v-if="notifications.length !== 0" class="text-[1.2em]" />
|
||||
<IconMdiBellOutline v-if="!unreadNotifications" class="text-[1.2em]" />
|
||||
<IconMdiBellBadge v-if="unreadNotifications" class="text-[1.2em]" />
|
||||
</div>
|
||||
</MenuButton>
|
||||
<MenuItems class="absolute flex flex-col mt-1 z-10 py-1 rounded border-t-2 border-primary-400 background-default drop-shadow-xl">
|
||||
<MenuItem v-for="notification in notifications" :key="notification.id" :class="'text-' + notification.type + ' flex shadow-0 p-2 ml-3 mr-2'">
|
||||
<MenuItems class="absolute flex flex-col mt-1 z-10 rounded border-t-2 border-primary-400 background-default drop-shadow-xl overflow-auto">
|
||||
<div v-if="notifications.length === 0">
|
||||
<span class="flex shadow-0 p-2 mt-1 ml-3 mr-2">{{ i18n.t("notifications.empty.recent") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:class="'text-sm flex shadow-0 p-3 pr-4 inline-flex items-center ' + (!notification.read ? 'bg-blue-100 dark:bg-slate-700' : '')"
|
||||
>
|
||||
<div class="text-lg mr-2">
|
||||
<IconMdiInformationOutline v-if="notification.type === 'info'" class="text-lightBlue-600" />
|
||||
<IconMdiCheck v-else-if="notification.type === 'success'" class="text-lime-600" />
|
||||
<IconMdiAlertOutline v-else-if="notification.type === 'warning'" class="text-red-600" />
|
||||
</div>
|
||||
{{ i18n.t(notification.message[0], notification.message.slice(1)) }}
|
||||
</MenuItem>
|
||||
<Link to="/notifications"><span class="ml-3 text-sm">View all notifications</span></Link>
|
||||
</div>
|
||||
<div class="ml-3 mb-1 space-x-3 text-sm">
|
||||
<Link to="/notifications"
|
||||
><span>{{ i18n.t("notifications.viewAll") }}</span></Link
|
||||
>
|
||||
<span v-if="unreadNotifications" class="color-primary font-bold hover:(underline)" @click="markNotificationRead">Mark as read</span>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
@ -556,6 +556,7 @@
|
||||
"unread": "Unread",
|
||||
"read": "Read",
|
||||
"all": "All",
|
||||
"viewAll": "View all notifications",
|
||||
"invite": {
|
||||
"all": "All",
|
||||
"projects": "Projects",
|
||||
@ -573,6 +574,7 @@
|
||||
},
|
||||
"empty": {
|
||||
"unread": "You have no unread notifications.",
|
||||
"recent": "You have no recent notifications!",
|
||||
"read": "You have no read notifications.",
|
||||
"all": "You have no notifications.",
|
||||
"invites": "You have no invites"
|
||||
|
2
frontend-new/src/types/internal/users.d.ts
vendored
2
frontend-new/src/types/internal/users.d.ts
vendored
@ -9,7 +9,7 @@ declare module "hangar-internal" {
|
||||
message: string[];
|
||||
read: boolean;
|
||||
originUserName: string | null;
|
||||
type: string;
|
||||
type: "neutral" | "success" | "info" | "warning" | "error";
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
@ -44,6 +44,10 @@ export default defineConfig({
|
||||
800: "#00102F",
|
||||
900: "#000817",
|
||||
},
|
||||
lightBlue: colors.lightBlue,
|
||||
blue: colors.blue,
|
||||
lime: colors.lime,
|
||||
slate: colors.slate,
|
||||
red: colors.red,
|
||||
gray: colors.zinc,
|
||||
secondary: colors.slate,
|
||||
|
@ -166,15 +166,9 @@ public class HangarUserController extends HangarComponent {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
switch (status) {
|
||||
case DECLINE:
|
||||
inviteService.declineInvite(table);
|
||||
break;
|
||||
case ACCEPT:
|
||||
inviteService.acceptInvite(table);
|
||||
break;
|
||||
case UNACCEPT:
|
||||
inviteService.unacceptInvite(table);
|
||||
break;
|
||||
case DECLINE -> inviteService.declineInvite(table);
|
||||
case ACCEPT -> inviteService.acceptInvite(table);
|
||||
case UNACCEPT -> inviteService.unacceptInvite(table);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ public interface HangarNotificationsDAO {
|
||||
" FROM notifications n" +
|
||||
" LEFT OUTER JOIN users u ON u.id = n.origin_id" +
|
||||
" WHERE n.user_id = :userId" +
|
||||
" ORDER BY n.created_at" +
|
||||
" ORDER BY n.created_at DESC" +
|
||||
" LIMIT :amount")
|
||||
List<HangarNotification> getNotifications(long userId, int amount);
|
||||
|
||||
@ -29,7 +29,7 @@ public interface HangarNotificationsDAO {
|
||||
" JOIN projects p ON p.id = upr.project_id" +
|
||||
" WHERE upr.user_id = :userId " +
|
||||
" AND upr.accepted = FALSE" +
|
||||
" ORDER BY upr.created_at")
|
||||
" ORDER BY upr.created_at DESC")
|
||||
List<HangarInvite.HangarProjectInvite> getProjectInvites(long userId);
|
||||
|
||||
@RegisterConstructorMapper(HangarInvite.HangarOrganizationInvite.class)
|
||||
@ -41,6 +41,6 @@ public interface HangarNotificationsDAO {
|
||||
" JOIN organizations o ON o.id = uor.organization_id" +
|
||||
" WHERE uor.user_id = :userId" +
|
||||
" AND uor.accepted = FALSE" +
|
||||
" ORDER BY uor.created_at")
|
||||
" ORDER BY uor.created_at DESC")
|
||||
List<HangarInvite.HangarOrganizationInvite> getOrganizationInvites(long userId);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public abstract class JoinableNotificationService<RT extends ExtendedRoleTable<?
|
||||
public void invited(Collection<RT> inviteeRoleTables, J joinable) {
|
||||
Collection<NotificationTable> notificationTables = new HashSet<>();
|
||||
for (RT rt : inviteeRoleTables) {
|
||||
notificationTables.add(new NotificationTable(rt.getUserId(), null, joinable.getId(), new String[]{this.msgPrefix + "invite", rt.getRole().getTitle(), joinable.getName()}, NotificationType.SUCCESS));
|
||||
notificationTables.add(new NotificationTable(rt.getUserId(), null, joinable.getId(), new String[]{this.msgPrefix + "invite", rt.getRole().getTitle(), joinable.getName()}, NotificationType.INFO));
|
||||
}
|
||||
notificationsDAO.insert(notificationTables);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user