Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0ec70d2
feat: implement instance share page + search_users backend call
IMB11 Jun 30, 2026
7c7f358
feat: invite players modal
IMB11 Jun 30, 2026
e9c1412
feat: use tanstack queries for friends sync across app pages
IMB11 Jun 30, 2026
23ce9bc
feat: base shared instances implementation
IMB11 Jun 30, 2026
4296e3b
fix: admon style
IMB11 Jun 30, 2026
52c38f7
feat: impl instance admonitions like server panel
IMB11 Jun 30, 2026
60fa3b7
fix: impl get + del usage
IMB11 Jul 1, 2026
d6bd6a2
feat: support modpack links
IMB11 Jul 2, 2026
fa286a7
feat: invite notif accepting
IMB11 Jul 2, 2026
7726b2a
fix: lint + fmt
IMB11 Jul 2, 2026
2351195
feat: impl install to play
IMB11 Jul 2, 2026
2da493e
feat: impl usage of UpdateToPlayModal
IMB11 Jul 2, 2026
70f0827
feat: warnings on deleting/disabling shared-instance version content
IMB11 Jul 2, 2026
1286480
Merge remote-tracking branch 'origin/main' into cal/peer-to-peer
IMB11 Jul 2, 2026
50e776b
fix: send instance name
IMB11 Jul 3, 2026
2dbc982
Merge remote-tracking branch 'origin' into cal/peer-to-peer
IMB11 Jul 3, 2026
4f5f3bf
feat: align with backend
IMB11 Jul 6, 2026
5b86bb8
Merge remote-tracking branch 'origin/main' into cal/peer-to-peer
IMB11 Jul 7, 2026
0723ef0
feat: shared instances qa
IMB11 Jul 7, 2026
d88d555
feat: wrong account protection
IMB11 Jul 7, 2026
a1ea690
feat: qa
IMB11 Jul 7, 2026
44ce6bf
fix: smartly apply updates
IMB11 Jul 7, 2026
38f02ea
fix: install bug
IMB11 Jul 7, 2026
92f8eb4
fix: 401/404 differentiation
IMB11 Jul 7, 2026
a34f5b1
fix: fmt+prepr
IMB11 Jul 7, 2026
20ed8e2
feat: qa
IMB11 Jul 8, 2026
17852f9
feat: qa
IMB11 Jul 8, 2026
a1876eb
fix: signing out messes up revoke/deleted checks
IMB11 Jul 8, 2026
a562bf9
feat: qa
IMB11 Jul 8, 2026
5511662
fix: fmt + lint
IMB11 Jul 8, 2026
b945871
Merge remote-tracking branch 'origin/main' into cal/peer-to-peer
IMB11 Jul 8, 2026
dbea158
feat: lock content if part of shared instance
IMB11 Jul 8, 2026
1067894
fix: lint
IMB11 Jul 8, 2026
6da553b
[do not merge] feat: rough invite links impl temp (#6666)
IMB11 Jul 9, 2026
adc1094
fix: wrong cmd
IMB11 Jul 9, 2026
de1bc58
feat: invite page
IMB11 Jul 10, 2026
1572087
fix: server-manager DTO mismatch
IMB11 Jul 10, 2026
dcfa572
fix: drop anonymous invite link acceptance
IMB11 Jul 10, 2026
ed87b62
refactor: structured shared-instance unavailable errors
IMB11 Jul 10, 2026
e8ea872
Merge remote-tracking branch 'origin/main' into cal/peer-to-peer
IMB11 Jul 10, 2026
70cc6c3
refactor: centralise error presentations
IMB11 Jul 10, 2026
9bdb1ef
refactor: dedupe shared instance diff detection
IMB11 Jul 10, 2026
d8633df
fix: logging in reqwests
IMB11 Jul 10, 2026
13588fc
refactor: move app.vue shared instances into handler
IMB11 Jul 10, 2026
d1ca6f4
refactor: break up Share.vue
IMB11 Jul 10, 2026
b50d1d0
refactor: split up shared instances state outside of instance index
IMB11 Jul 10, 2026
913dde9
refactor: dedicated shared instances install/update modals + split up…
IMB11 Jul 10, 2026
63a60b2
refactor: centralized managed content
IMB11 Jul 10, 2026
47cd750
refactor: split up install shared to own runner + shared.rs split up
IMB11 Jul 10, 2026
93a1fc2
refactor: dedupe sql for instance metadata enrichmnt
IMB11 Jul 10, 2026
fe67728
refactor: friends composable + dedupe friends logic across usages
IMB11 Jul 10, 2026
877afa7
chore: reduced unused code
IMB11 Jul 10, 2026
ad969a3
fix: align with backend
IMB11 Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 63 additions & 28 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
HomeIcon,
LeftArrowIcon,
LibraryIcon,
LinkIcon,
LogInIcon,
LogOutIcon,
NewspaperIcon,
Expand Down Expand Up @@ -85,6 +86,7 @@ import NavButton from '@/components/ui/NavButton.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
Expand Down Expand Up @@ -147,6 +149,7 @@ const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
let credentialsRefreshId = 0
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
Expand Down Expand Up @@ -637,6 +640,7 @@ const contentInstallModpackAlreadyInstalledModal = ref()
const addServerToInstanceModal = ref()
const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const sharedInstanceInviteHandler = ref()
const updateToPlayModal = ref()

const modrinthLoginFlowWaitModal = ref()
Expand All @@ -647,8 +651,8 @@ watch(incompatibilityWarningModal, (modal) => {
}
})

setupAuthProvider(credentials, async (_redirectPath) => {
await signIn()
setupAuthProvider(credentials, async (_redirectPath, flow) => {
await signIn(flow)
})

async function validateSession(sessionToken) {
Expand All @@ -665,23 +669,33 @@ async function validateSession(sessionToken) {
}

async function fetchCredentials() {
const refreshId = ++credentialsRefreshId
credentials.value = undefined

const creds = await getCreds().catch(handleError)
if (refreshId !== credentialsRefreshId) return

if (creds && creds.user_id) {
if (creds.session && !(await validateSession(creds.session))) {
if (refreshId !== credentialsRefreshId) return

await logout().catch(handleError)
if (refreshId !== credentialsRefreshId) return

credentials.value = null
return
}
creds.user = await get_user(creds.user_id, 'bypass').catch(handleError)
if (refreshId !== credentialsRefreshId) return
}
credentials.value = creds ?? null
}

async function signIn() {
async function signIn(flow = 'sign-in') {
modrinthLoginFlowWaitModal.value.show()

try {
await login()
await login(flow)
await fetchCredentials()
} catch (error) {
if (
Expand All @@ -699,6 +713,13 @@ async function signIn() {
}

async function logOut() {
await performLogOut()
}

async function performLogOut() {
credentialsRefreshId++
credentials.value = undefined

await logout().catch(handleError)
await fetchCredentials()
}
Expand Down Expand Up @@ -821,30 +842,35 @@ function openServerInviteInviterProfile(inviterName) {
}

async function handleLiveNotification(notification) {
if (notification?.body?.type !== 'server_invite' || notification.read) return
if (displayedServerInviteNotifications.has(notification.id)) return

displayedServerInviteNotifications.add(notification.id)

const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null

addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
if (!notification?.body || notification.read) return
if (await sharedInstanceInviteHandler.value?.handleNotification(notification)) return

if (notification.body.type === 'server_invite') {
if (displayedServerInviteNotifications.has(notification.id)) return

displayedServerInviteNotifications.add(notification.id)

const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null

addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
}

}

async function handleCommand(e) {
Expand Down Expand Up @@ -877,6 +903,8 @@ async function handleCommand(e) {
} else {
await run(e.id).catch(handleError)
}
} else if (e.event === 'InstallSharedInstanceInvite') {
await sharedInstanceInviteHandler.value?.installFromInviteId(e.invite_id)
} else if (e.event === 'InstallServer') {
await router.push(`/project/${e.id}`)
await playServerProject(e.id).catch(handleError)
Expand Down Expand Up @@ -1467,6 +1495,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
<PlusIcon />
</NavButton>
<NavButton
v-tooltip.right="'Install from invite link'"
:to="(event) => sharedInstanceInviteHandler?.showManualInviteLinkModal(event)"
>
<LinkIcon />
</NavButton>
<div class="flex flex-grow"></div>
<NavButton
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
Expand Down Expand Up @@ -1736,6 +1770,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@go-to-instance="handleContentInstallModpackDuplicateGoToInstance"
/>
<SharedInstanceInviteHandler ref="sharedInstanceInviteHandler" />
<InstallToPlayModal ref="installToPlayModal" />
<UpdateToPlayModal ref="updateToPlayModal" />
</template>
Expand Down
4 changes: 3 additions & 1 deletion apps/app-frontend/src/components/ui/Instance.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ const unlisten = await process_listener((e) => {
}
})

onMounted(() => checkProcess())
onMounted(() => {
checkProcess()
})
onUnmounted(() => unlisten())
</script>

Expand Down
91 changes: 26 additions & 65 deletions apps/app-frontend/src/components/ui/friends/FriendsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, ref } from 'vue'

import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
import { useFriends } from '@/composables/use-friends'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { friend_listener } from '@/helpers/events'
import {
add_friend,
friends,
type FriendWithUserData,
remove_friend,
transformFriends,
} from '@/helpers/friends.ts'
import type { FriendWithUserData } from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'

const { formatMessage } = useVIntl()
Expand All @@ -35,36 +29,23 @@ const props = defineProps<{
}>()

const userCredentials = computed(() => props.credentials)
const {
friends: userFriends,
loading,
requestFriend,
acceptFriend,
removeFriend: removeFriendRecord,
} = useFriends({
currentUserId: () => userCredentials.value?.user_id,
getCredentials: () => userCredentials.value,
onError: handleError,
})

const search = ref('')
const friendInvitesModal = ref()

const username = ref('')
const addFriendModal = ref()
async function addFriendFromModal() {
addFriendModal.value.hide()
await add_friend(username.value).catch(handleError)
username.value = ''
await loadFriends()
}

async function addFriend(friend: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await add_friend(id).catch(handleError)
await loadFriends()
}
}

async function removeFriend(friend: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await remove_friend(id).catch(handleError)
await loadFriends()
}
}

const userFriends = ref<FriendWithUserData[]>([])
const sortedFriends = computed<FriendWithUserData[]>(() =>
userFriends.value.slice().sort((a, b) => {
if (a.last_updated === null && b.last_updated === null) {
Expand Down Expand Up @@ -108,42 +89,22 @@ const incomingRequests = computed(() =>
.sort((a, b) => b.created.diff(a.created)),
)

const loading = ref(true)
async function loadFriends(timeout = false) {
loading.value = timeout
function addFriendFromModal() {
const target = username.value.trim()
if (!target) return

try {
const friendsList = await friends()
userFriends.value = await transformFriends(friendsList, userCredentials.value)
loading.value = false
} catch (e) {
console.error('Error loading friends', e)
if (timeout) {
setTimeout(() => loadFriends(), 15 * 1000)
}
}
addFriendModal.value.hide()
requestFriend({ id: target, username: target })
username.value = ''
}

watch(
userCredentials,
() => {
if (userCredentials.value === undefined) {
userFriends.value = []
loading.value = false
} else if (userCredentials.value === null) {
userFriends.value = []
loading.value = false
} else {
loadFriends(true)
}
},
{ immediate: true },
)
function addFriend(friend: FriendWithUserData) {
acceptFriend(friend)
}

const unlisten = await friend_listener(() => loadFriends())
onUnmounted(() => {
unlisten()
})
function removeFriend(friend: FriendWithUserData) {
removeFriendRecord(friend)
}

const messages = defineMessages({
addFriend: {
Expand Down
Loading
Loading