From a0a859aa868cdf56bd81534404e9d85656e0a808 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:33:53 -0500 Subject: [PATCH] [ShareBubbles] Fixes for 2025r180 --- ShareBubbles/Main.cs | 2 +- ShareBubbles/Patches.cs | 1631 +---------------- ShareBubbles/Properties/AssemblyInfo.cs | 4 +- .../API/Exceptions/ShareApiExceptions.cs | 66 - .../API/PedestalInfoBatchProcessor.cs | 3 +- .../API/Responses/ActiveSharesResponse.cs | 22 - .../ShareBubbles/API/ShareApiHelper.cs | 237 --- .../Implementation/AvatarBubbleImpl.cs | 4 +- .../Implementation/SpawnableBubbleImpl.cs | 4 +- .../Implementation/TempShareManager.cs | 4 +- .../Networking/ModNetwork.Inbound.cs | 4 +- .../Networking/ModNetwork.Outbound.cs | 4 +- .../ShareBubbles/UI/BubbleInteract.cs | 18 +- ShareBubbles/format.json | 10 +- 14 files changed, 60 insertions(+), 1953 deletions(-) delete mode 100644 ShareBubbles/ShareBubbles/API/Exceptions/ShareApiExceptions.cs delete mode 100644 ShareBubbles/ShareBubbles/API/Responses/ActiveSharesResponse.cs delete mode 100644 ShareBubbles/ShareBubbles/API/ShareApiHelper.cs diff --git a/ShareBubbles/Main.cs b/ShareBubbles/Main.cs index 72f3c3d..802a6be 100644 --- a/ShareBubbles/Main.cs +++ b/ShareBubbles/Main.cs @@ -28,7 +28,7 @@ public class ShareBubblesMod : MelonMod LoadAssetBundle(); } - + public override void OnApplicationQuit() { ModNetwork.Unsubscribe(); diff --git a/ShareBubbles/Patches.cs b/ShareBubbles/Patches.cs index 5f95231..eaf2440 100644 --- a/ShareBubbles/Patches.cs +++ b/ShareBubbles/Patches.cs @@ -1,14 +1,6 @@ using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.Networking.API.Responses; -using ABI_RC.Core.Networking.API.UserWebsocket; using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; using HarmonyLib; -using MTJobSystem; -using NAK.ShareBubbles.API; -using NAK.ShareBubbles.API.Exceptions; -using NAK.ShareBubbles.API.Responses; -using Newtonsoft.Json; namespace NAK.ShareBubbles.Patches; @@ -68,1419 +60,31 @@ internal static class ControllerRay_Patches internal static class ViewManager_Patches { - -private const string DETAILS_TOOLBAR_PATCHES = """ - -const ContentShareMod = { - debugMode: false, - currentContentData: null, - themeColors: null, - - /* Theme Handling */ - - getThemeColors: function() { - if (this.themeColors) return this.themeColors; - - // Default fallback colors - const defaultColors = { - background: '#373021', - border: '#59885d' - }; - - // Try to get colors from favorite category element - const favoriteCategoryElement = document.querySelector('.favorite-category-selection'); - if (!favoriteCategoryElement) return defaultColors; - - const computedStyle = window.getComputedStyle(favoriteCategoryElement); - this.themeColors = { - background: computedStyle.backgroundColor || defaultColors.background, - border: computedStyle.borderColor || defaultColors.border - }; - - return this.themeColors; - }, - - applyThemeToDialog: function(dialog) { - const colors = this.getThemeColors(); - dialog.style.backgroundColor = colors.background; - dialog.style.borderColor = colors.border; - - // Update any close or page buttons to match theme - const buttons = dialog.querySelectorAll('.close-btn, .page-btn'); - buttons.forEach(button => { - button.style.borderColor = colors.border; - }); - - return colors; - }, - - /* Core Initialization */ - - init: function() { - const styles = [ - this.getSharedStyles(), - this.ShareBubble.initStyles(), - this.ShareSelect.initStyles(), - this.DirectShare.initStyles(), - this.Unshare.initStyles() - ].join('\n'); - - const styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.innerHTML = styles; - document.head.appendChild(styleElement); - - this.shareBubbleDialog = this.ShareBubble.createDialog(); - this.shareSelectDialog = this.ShareSelect.createDialog(); - this.directShareDialog = this.DirectShare.createDialog(); - this.unshareDialog = this.Unshare.createDialog(); - - this.initializeToolbars(); - this.bindEvents(); - }, - - getSharedStyles: function() { - return ` - .content-sharing-base-dialog { - position: fixed; - background-color: #373021; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 800px; - min-width: 500px; - border: 3px solid #59885d; - padding: 20px; - z-index: 100000; - opacity: 0; - transition: opacity 0.2s linear; - } - - .content-sharing-base-dialog.in { - opacity: 1; - } - - .content-sharing-base-dialog.out { - opacity: 0; - } - - .content-sharing-base-dialog.hidden { - display: none; - } - - .content-sharing-base-dialog h2, - .content-sharing-base-dialog h3 { - margin-top: 0; - margin-bottom: 0.5em; - text-align: left; - } - - .content-sharing-base-dialog .description { - margin-bottom: 1em; - text-align: left; - font-size: 0.9em; - color: #aaa; - } - - .content-sharing-base-dialog .close-btn { - position: absolute; - top: 1%; - right: 1%; - border-radius: 0.25em; - border: 3px solid #59885d; - padding: 0.5em; - width: 8em; - text-align: center; - } - - .page-btn { - border-radius: 0.25em; - border: 3px solid #59885d; - padding: 0.5em; - width: 8em; - text-align: center; - } - - .page-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .inp-hidden { - display: none; - } - `; - }, - - /* Feature Modules */ - - ShareBubble: { - initStyles: function() { - return ` - .share-bubble-dialog { - max-width: 800px; - transform: translate(-50%, -60%); - } - - .share-bubble-dialog .content-btn { - position: relative; - margin-bottom: 0.5em; - } - - .share-bubble-dialog .btn-group { - display: flex; - margin-bottom: 1em; - } - - .share-bubble-dialog .option-select { - flex: 1 1 0; - min-width: 0; - background-color: inherit; - text-align: center; - padding: 0.5em; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid inherit; - } - - .share-bubble-dialog .option-select + .option-select { - margin-left: -1px; - } - - .share-bubble-dialog .option-select.active, - .share-bubble-dialog .option-select:hover { - background-color: rgba(27, 80, 55, 1); - } - - .share-bubble-dialog .action-buttons { - margin-top: 1em; - display: flex; - justify-content: space-between; - } - - .share-bubble-dialog .action-btn { - flex: 1; - text-align: center; - padding: 0.5em; - border-radius: 0.25em; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - margin-right: 0.5em; - } - - .share-bubble-dialog .action-btn:last-child { - margin-right: 0; - } - `; - }, - - createDialog: function() { - const dialog = document.createElement('div'); - dialog.id = 'content-share-bubble-dialog'; - dialog.className = 'content-sharing-base-dialog share-bubble-dialog hidden'; - dialog.innerHTML = ` -

Share Bubble

-
Close
- -

Visibility

-

Choose who can see your Sharing Bubble.

-
-
Everyone
-
Friends Only
-
- -

Lifetime

-

How long the Sharing Bubble lasts. You can delete it at any time.

-
-
2 Minutes
-
For Session
-
- -
-

Access Control

-
- - -

- Users cannot access your Private Content through this share. -

-
-
-
Keep
-
Instance Only
-
None
-
-
- - - - - - - -
-
Drop
-
Select
-
- `; - document.body.appendChild(dialog); - return dialog; - }, - - show: function(contentDetails) { - const dialog = ContentShareMod.shareBubbleDialog; - const colors = ContentShareMod.applyThemeToDialog(dialog); - - // Additional ShareBubble-specific theming - const optionSelects = dialog.querySelectorAll('.option-select'); - optionSelects.forEach(element => { - element.style.borderColor = colors.border; - }); - - const grantAccessSection = dialog.querySelector('.grant-access-section'); - const noAccessControlMessage = dialog.querySelector('.no-access-control-message'); - const showGrantAccess = contentDetails.IsMine && !contentDetails.IsPublic; - - if (grantAccessSection && noAccessControlMessage) { - grantAccessSection.style.display = showGrantAccess ? '' : 'none'; - noAccessControlMessage.style.display = showGrantAccess ? 'none' : ''; - } - - dialog.classList.remove('hidden', 'out'); - setTimeout(() => dialog.classList.add('in'), 50); - - ContentShareMod.currentContentData = contentDetails; - }, - - hide: function() { - const dialog = ContentShareMod.shareBubbleDialog; - dialog.classList.remove('in'); - dialog.classList.add('out'); - setTimeout(() => { - dialog.classList.add('hidden'); - dialog.classList.remove('out'); - }, 200); - }, - - changeVisibility: function(element) { - document.getElementById('share-visibility').value = element.dataset.visibilityValue; - const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.visibility-btn'); - buttons.forEach(btn => btn.classList.remove('active')); - element.classList.add('active'); - }, - - changeDuration: function(element) { - document.getElementById('share-duration').value = element.dataset.durationValue; - const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.duration-btn'); - buttons.forEach(btn => btn.classList.remove('active')); - element.classList.add('active'); - }, - - changeAccess: function(element) { - document.getElementById('share-access').value = element.dataset.accessValue; - const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.access-btn'); - buttons.forEach(btn => btn.classList.remove('active')); - element.classList.add('active'); - - const descriptions = ContentShareMod.shareBubbleDialog.querySelectorAll('.access-desc'); - descriptions.forEach(desc => { - desc.style.display = desc.dataset.accessType === element.dataset.accessValue ? '' : 'none'; - }); - }, - - submit: function(action) { - const contentDetails = ContentShareMod.currentContentData; - let bubbleImpl, bubbleContent, contentImage, contentName; - - if (contentDetails.AvatarId) { - bubbleImpl = 'Avatar'; - bubbleContent = contentDetails.AvatarId; - contentImage = contentDetails.AvatarImageCoui; - contentName = contentDetails.AvatarName; - } else if (contentDetails.SpawnableId) { - bubbleImpl = 'Spawnable'; - bubbleContent = contentDetails.SpawnableId; - contentImage = contentDetails.SpawnableImageCoui; - contentName = contentDetails.SpawnableName; - } else if (contentDetails.WorldId) { - bubbleImpl = 'World'; - bubbleContent = contentDetails.WorldId; - contentImage = contentDetails.WorldImageCoui; - contentName = contentDetails.WorldName; - } else if (contentDetails.UserId) { - bubbleImpl = 'User'; - bubbleContent = contentDetails.UserId; - contentImage = contentDetails.UserImageCoui; - contentName = contentDetails.UserName; - } else { - console.error('No valid content ID found'); - return; - } - - const visibility = document.getElementById('share-visibility').value; - const duration = document.getElementById('share-duration').value; - const access = document.getElementById('share-access').value; - - const shareRule = visibility === 'FriendsOnly' ? 'FriendsOnly' : 'Everyone'; - const shareLifetime = duration === 'Session' ? 'Session' : 'TwoMinutes'; - const shareAccess = access === 'Permanent' ? 'Permanent' : - access === 'Session' ? 'Session' : 'NoAccess'; - - if (ContentShareMod.debugMode) { - console.log('Sharing content:', { - action, bubbleImpl, bubbleContent, - shareRule, shareLifetime, shareAccess - }); - } - - engine.call('NAKCallShareContent', action, bubbleImpl, bubbleContent, - shareRule, shareLifetime, shareAccess, contentImage, contentName); - - this.hide(); - } - }, - - Unshare: { - currentPage: 1, - totalPages: 1, - sharesPerPage: 5, - sharesList: null, - - initStyles: function() { - return ` - .unshare-dialog { - width: 800px; - height: 1000px; - transform: translate(-50%, -60%); - display: flex; - flex-direction: column; - } - - .unshare-dialog .shares-container { - flex: 1; - overflow-y: auto; - margin: 20px 0; - min-height: 0; - } - - .unshare-dialog #shares-loading, - .unshare-dialog #shares-error, - .unshare-dialog #shares-empty { - text-align: center; - padding: 2em; - font-size: 1.1em; - } - - .unshare-dialog #shares-error { - color: #ff6b6b; - } - - .unshare-dialog .share-item { - display: flex; - align-items: center; - padding: 15px; - border: 1px solid rgba(255, 255, 255, 0.1); - margin-bottom: 10px; - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; - min-height: 120px; - } - - .unshare-dialog .share-item img { - width: 96px; - height: 96px; - border-radius: 4px; - margin-right: 15px; - cursor: pointer; - } - - .unshare-dialog .share-item .user-name { - flex: 1; - font-size: 1.3em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 15px; - cursor: pointer; - } - - .unshare-dialog .action-btn { - width: 180px; - height: 60px; - font-size: 1.2em; - color: white; - border-radius: 4px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 10px; - padding: 0 20px; - text-align: center; - } - - .unshare-dialog .revoke-btn { - background-color: #ff6b6b; - } - - .unshare-dialog .undo-btn { - background-color: #4a9eff; - } - - .unshare-dialog .action-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - background-color: #666; - } - - .unshare-dialog .pagination { - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding-top: 20px; - } - - .unshare-dialog .page-info { - font-size: 1.1em; - opacity: 0.8; - margin-bottom: 15px; - text-align: center; - } - - .unshare-dialog .page-buttons { - display: flex; - justify-content: center; - gap: 20px; - } - - .unshare-dialog .share-item.revoked { - opacity: 0.7; - } - `; - }, - - createDialog: function() { - const dialog = document.createElement('div'); - dialog.id = 'content-unshare-dialog'; - dialog.className = 'content-sharing-base-dialog unshare-dialog hidden'; - dialog.innerHTML = ` -

Manage Shares

-
Close
- -
-
Loading shares...
- - - -
- - - `; - document.body.appendChild(dialog); - return dialog; - }, - - show: function(contentDetails) { - const dialog = ContentShareMod.unshareDialog; - ContentShareMod.applyThemeToDialog(dialog); - - dialog.classList.remove('hidden', 'out'); - setTimeout(() => dialog.classList.add('in'), 50); - - ContentShareMod.currentContentData = contentDetails; - this.currentPage = 1; - this.totalPages = 1; - this.sharesList = null; - this.requestShares(); - }, - - hide: function() { - const dialog = ContentShareMod.unshareDialog; - dialog.classList.remove('in'); - dialog.classList.add('out'); - setTimeout(() => { - dialog.classList.add('hidden'); - dialog.classList.remove('out'); - }, 200); - }, - - requestShares: function() { - const dialog = ContentShareMod.unshareDialog; - const sharesContainer = dialog.querySelector('.shares-container'); - - sharesContainer.querySelector('#shares-loading').style.display = ''; - sharesContainer.querySelector('#shares-error').style.display = 'none'; - sharesContainer.querySelector('#shares-empty').style.display = 'none'; - sharesContainer.querySelector('#shares-list').style.display = 'none'; - - const contentDetails = ContentShareMod.currentContentData; - const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable'; - const contentId = contentDetails.AvatarId || contentDetails.SpawnableId; - - engine.call('NAKGetContentShares', contentType, contentId); - }, - - handleSharesResponse: function(success, shares) { - const dialog = ContentShareMod.unshareDialog; - const sharesContainer = dialog.querySelector('.shares-container'); - const loadingElement = sharesContainer.querySelector('#shares-loading'); - const errorElement = sharesContainer.querySelector('#shares-error'); - const emptyElement = sharesContainer.querySelector('#shares-empty'); - const sharesListElement = sharesContainer.querySelector('#shares-list'); - - loadingElement.style.display = 'none'; - - if (!success) { - errorElement.style.display = ''; - return; - } - - try { - const response = JSON.parse(shares); - this.sharesList = response.Data.value; - - if (!this.sharesList || this.sharesList.length === 0) { - emptyElement.style.display = ''; - const pagination = dialog.querySelector('.pagination'); - const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn'); - prevButton.disabled = true; - nextButton.disabled = true; - pagination.querySelector('.page-info').textContent = '1/1'; - return; - } - - this.totalPages = Math.ceil(this.sharesList.length / this.sharesPerPage); - this.updatePageContent(); - } catch (error) { - console.error('Error parsing shares:', error); - errorElement.style.display = ''; - } - }, - - updatePageContent: function() { - const dialog = ContentShareMod.unshareDialog; - const sharesListElement = dialog.querySelector('#shares-list'); - - const startIndex = (this.currentPage - 1) * this.sharesPerPage; - const endIndex = startIndex + this.sharesPerPage; - const currentShares = this.sharesList.slice(startIndex, endIndex); - - sharesListElement.innerHTML = currentShares.map(share => ` -
- ${share.name}'s avatar - ${share.name} - -
- `).join(''); - - sharesListElement.style.display = ''; - - const pagination = dialog.querySelector('.pagination'); - pagination.querySelector('.page-info').textContent = `${this.currentPage}/${this.totalPages}`; - const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn'); - prevButton.disabled = this.currentPage === 1; - nextButton.disabled = this.currentPage === this.totalPages; - }, - - previousPage: function() { - if (this.currentPage > 1) { - this.currentPage--; - this.updatePageContent(); - } - }, - - nextPage: function() { - if (this.currentPage < this.totalPages) { - this.currentPage++; - this.updatePageContent(); - } - }, - - viewUserProfile: function(userId) { - this.hide(); - getUserDetails(userId); - }, - - revokeShare: function(userId, buttonElement) { - const contentDetails = ContentShareMod.currentContentData; - const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable'; - const contentId = contentDetails.AvatarId || contentDetails.SpawnableId; - - buttonElement.disabled = true; - buttonElement.textContent = 'Revoking...'; - - engine.call('NAKRevokeContentShare', contentType, contentId, userId); - }, - - handleRevokeResponse: function(success, userId, error) { - const dialog = ContentShareMod.unshareDialog; - const shareItem = dialog.querySelector(`[data-user-id="${userId}"]`); - if (!shareItem) return; - - const actionButton = shareItem.querySelector('button'); - - if (success) { - shareItem.classList.add('revoked'); - actionButton.className = 'action-btn undo-btn button'; - actionButton.textContent = 'Undo'; - actionButton.onclick = () => { - actionButton.disabled = true; - actionButton.textContent = 'Restoring...'; - - const contentDetails = ContentShareMod.currentContentData; - const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable'; - const contentId = contentDetails.AvatarId || contentDetails.SpawnableId; - - engine.call('NAKCallShareContentDirect', contentType, contentId, userId); - }; - uiPushShow("Share revoked successfully", 3); - } else { - actionButton.textContent = 'Failed'; - actionButton.classList.add('failed'); - uiPushShow(error || "Failed to revoke share", 3); - - // Reset button after a moment - setTimeout(() => { - actionButton.disabled = false; - actionButton.textContent = 'Revoke'; - actionButton.classList.remove('failed'); - }, 1000); - } - }, - - handleShareResponse: function(success, userId, error) { - const dialog = ContentShareMod.unshareDialog; - const shareItem = dialog.querySelector(`[data-user-id="${userId}"]`); - if (!shareItem) return; - - const actionButton = shareItem.querySelector('button'); - - if (success) { - this.requestShares(); - uiPushShow("Share restored successfully", 3); - } else { - actionButton.textContent = 'Failed'; - actionButton.classList.add('failed'); - uiPushShow(error || "Failed to restore share", 3); - - // Reset button after a moment - setTimeout(() => { - actionButton.disabled = false; - actionButton.textContent = 'Undo'; - actionButton.classList.remove('failed'); - }, 1000); - } - } - }, - - DirectShare: { - currentPage: 1, - totalPages: 1, - usersPerPage: 5, - usersList: null, - isInstanceUsers: true, - - initStyles: function() { - return ` - .direct-share-dialog { - width: 800px; - height: 1000px; - transform: translate(-50%, -60%); - display: flex; - flex-direction: column; - } - - .direct-share-dialog .search-container { - margin: 20px 0; - padding: 10px; - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; - } - - .direct-share-dialog .search-input { - width: 100%; - padding: 10px; - border: none; - background: transparent; - color: inherit; - font-size: 1.1em; - } - - .direct-share-dialog .source-indicator { - padding: 10px; - text-align: center; - opacity: 0.8; - background: rgba(0, 0, 0, 0.1); - border-radius: 4px; - margin-bottom: 20px; - } - - .direct-share-dialog .users-container { - flex: 1; - overflow-y: auto; - margin-bottom: 20px; - min-height: 0; - } - - .direct-share-dialog .user-item { - display: flex; - align-items: center; - padding: 15px; - border: 1px solid rgba(255, 255, 255, 0.1); - margin-bottom: 10px; - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; - min-height: 96px; - } - - .direct-share-dialog .user-item img { - width: 96px; - height: 96px; - margin-right: 15px; - cursor: pointer; - border-radius: 4px; - } - - .direct-share-dialog .user-item .user-name { - flex: 1; - font-size: 1.3em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 15px; - cursor: pointer; - } - - .direct-share-dialog .user-item .action-btn { - width: 140px; - height: 50px; - font-size: 1.2em; - color: white; - background-color: rgba(27, 80, 55, 1); - border-radius: 4px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - .direct-share-dialog .user-item .action-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - background-color: #4a9eff; - } - - .direct-share-dialog .pagination { - border-top: 1px solid rgba(255, 255, 255, 0.1); - padding-top: 20px; - } - - .direct-share-dialog .page-info { - font-size: 1.1em; - opacity: 0.8; - margin-bottom: 15px; - text-align: center; - } - - .direct-share-dialog .page-buttons { - display: flex; - justify-content: center; - gap: 20px; - } - - .direct-share-dialog .user-item .action-btn { - width: 180px; - height: 60px; - font-size: 1.2em; - color: white; - border-radius: 4px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 10px; - padding: 0 20px; - text-align: center; - background-color: rgba(27, 80, 55, 1); - } - - .direct-share-dialog .user-item .action-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .direct-share-dialog .user-item .action-btn.shared { - background-color: #4a9eff; - } - - .direct-share-dialog .user-item .action-btn.failed { - background-color: #ff6b6b; - } - `; - }, - - createDialog: function() { - const dialog = document.createElement('div'); - dialog.id = 'content-direct-share-dialog'; - dialog.className = 'content-sharing-base-dialog direct-share-dialog hidden'; - dialog.innerHTML = ` -

Direct Share

-
Close
- -
-
Loading users...
- - - -
- - - `; - document.body.appendChild(dialog); - return dialog; - }, - - show: function(contentDetails) { - const dialog = ContentShareMod.directShareDialog; - ContentShareMod.applyThemeToDialog(dialog); - - dialog.classList.remove('hidden', 'out'); - setTimeout(() => dialog.classList.add('in'), 50); - - ContentShareMod.currentContentData = contentDetails; - this.currentPage = 1; - this.totalPages = 1; - this.usersList = null; - this.requestUsers(true); - }, - - hide: function() { - const dialog = ContentShareMod.directShareDialog; - dialog.classList.remove('in'); - dialog.classList.add('out'); - setTimeout(() => { - dialog.classList.add('hidden'); - dialog.classList.remove('out'); - }, 200); - }, - - handleUsersResponse: function(success, users, isInstanceUsers) { - const dialog = ContentShareMod.directShareDialog; - const usersContainer = dialog.querySelector('.users-container'); - // const sourceIndicator = dialog.querySelector('.source-indicator'); - const loadingElement = usersContainer.querySelector('#users-loading'); - const errorElement = usersContainer.querySelector('#users-error'); - const emptyElement = usersContainer.querySelector('#users-empty'); - const usersListElement = usersContainer.querySelector('#users-list'); - - loadingElement.style.display = 'none'; - // sourceIndicator.textContent = isInstanceUsers ? - // 'Showing users in current instance' : - // 'Showing search results'; - - // TODO: Add source indicator to html: - //
- // Showing users in current instance - //
- - if (!success) { - errorElement.style.display = ''; - return; - } - - try { - const response = JSON.parse(users); - this.usersList = response.entries; - this.isInstanceUsers = isInstanceUsers; - - if (!this.usersList || this.usersList.length === 0) { - emptyElement.style.display = ''; - this.updatePagination(); - return; - } - - this.totalPages = Math.ceil(this.usersList.length / this.usersPerPage); - this.updatePageContent(); - } catch (error) { - console.error('Error parsing users:', error); - errorElement.style.display = ''; - } - }, - - handleSearch: function(event) { - if (event.key === 'Enter') { - const searchValue = event.target.value.trim(); - // Pass true for instance users when empty search, false for search results - this.requestUsers(searchValue === '', searchValue); - } - }, - - requestUsers: function(isInstanceUsers, searchQuery = '') { - const dialog = ContentShareMod.directShareDialog; - const usersContainer = dialog.querySelector('.users-container'); - - usersContainer.querySelector('#users-loading').style.display = ''; - usersContainer.querySelector('#users-error').style.display = 'none'; - usersContainer.querySelector('#users-empty').style.display = 'none'; - usersContainer.querySelector('#users-list').style.display = 'none'; - - engine.call('NAKGetUsersForSharing', searchQuery); - }, - - updatePageContent: function() { - const dialog = ContentShareMod.directShareDialog; - const usersListElement = dialog.querySelector('#users-list'); - - const startIndex = (this.currentPage - 1) * this.usersPerPage; - const endIndex = startIndex + this.usersPerPage; - const currentUsers = this.usersList.slice(startIndex, endIndex); - - usersListElement.innerHTML = currentUsers.map(user => ` -
- ${user.name}'s avatar - ${user.name} - -
- `).join(''); - - usersListElement.style.display = ''; - this.updatePagination(); - }, - - updatePagination: function() { - const dialog = ContentShareMod.directShareDialog; - const pagination = dialog.querySelector('.pagination'); - const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn'); - - pagination.querySelector('.page-info').textContent = `Page ${this.currentPage}/${this.totalPages}`; - prevButton.disabled = this.currentPage === 1; - nextButton.disabled = this.currentPage === this.totalPages; - }, - - previousPage: function() { - if (this.currentPage > 1) { - this.currentPage--; - this.updatePageContent(); - } - }, - - nextPage: function() { - if (this.currentPage < this.totalPages) { - this.currentPage++; - this.updatePageContent(); - } - }, - - viewUserProfile: function(userId) { - this.hide(); - getUserDetails(userId); - }, - - shareWithUser: function(userId, buttonElement) { - const contentDetails = ContentShareMod.currentContentData; - const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable'; - const contentId = contentDetails.AvatarId || contentDetails.SpawnableId; - const contentName = contentDetails.AvatarName || contentDetails.SpawnableName; - const contentImage = contentDetails.AvatarImageURL || contentDetails.SpawnableImageURL; - - buttonElement.disabled = true; - buttonElement.textContent = 'Sharing...'; - - engine.call('NAKCallShareContentDirect', contentType, contentId, userId, contentName, contentImage); - }, - - handleShareResponse: function(success, userId, error) { - const dialog = ContentShareMod.directShareDialog; - const userItem = dialog.querySelector(`[data-user-id="${userId}"]`); - if (!userItem) return; - - const actionButton = userItem.querySelector('button'); - - if (success) { - actionButton.textContent = 'Shared'; - actionButton.disabled = true; - actionButton.classList.add('shared'); - uiPushShow("Content shared successfully", 3, "shareresponse"); - } else { - actionButton.disabled = false; - actionButton.textContent = 'Failed'; - actionButton.classList.add('failed'); - uiPushShow(error || "Failed to share content", 3, "shareresponse"); - - // Reset button after a moment - setTimeout(() => { - actionButton.disabled = false; - actionButton.textContent = 'Share'; - actionButton.classList.remove('failed', 'shared'); - }, 1000); - } - } - }, - - ShareSelect: { - initStyles: function() { - return ` - .share-select-dialog { - width: 650px; - height: 480px; - transform: translate(-50%, -80%); - } - - .share-select-dialog .share-options { - display: flex; - flex-direction: column; - gap: 15px; - margin-top: 20px; - } - - .share-select-dialog .share-option { - padding: 20px; - text-align: left; - cursor: pointer; - background: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - transition: background-color 0.2s ease; - } - - .share-select-dialog .share-option:hover { - background-color: rgba(27, 80, 55, 1); - border-color: rgba(255, 255, 255, 0.2); - } - - .share-select-dialog h3 { - margin: 0 0 8px 0; - font-size: 1.2em; - } - - .share-select-dialog p { - margin: 0; - opacity: 0.8; - font-size: 0.95em; - line-height: 1.4; - } - `; - }, - - createDialog: function() { - const dialog = document.createElement('div'); - dialog.id = 'content-share-select-dialog'; - dialog.className = 'content-sharing-base-dialog share-select-dialog hidden'; - dialog.innerHTML = ` -

Share Content

-
Close
- -
-
-

Share Bubble

-

Drop or place a bubble in the world that others can interact with

-
- -
-

Direct Share

-

Share directly with specific users

-
-
- `; - document.body.appendChild(dialog); - return dialog; - }, - - show: function(contentDetails) { - const dialog = ContentShareMod.shareSelectDialog; - ContentShareMod.applyThemeToDialog(dialog); - - dialog.classList.remove('hidden', 'out'); - setTimeout(() => dialog.classList.add('in'), 50); - - ContentShareMod.currentContentData = contentDetails; - }, - - hide: function() { - const dialog = ContentShareMod.shareSelectDialog; - dialog.classList.remove('in'); - dialog.classList.add('out'); - setTimeout(() => { - dialog.classList.add('hidden'); - dialog.classList.remove('out'); - }, 200); - }, - - openShareBubble: function() { - this.hide(); - ContentShareMod.ShareBubble.show(ContentShareMod.currentContentData); - }, - - openDirectShare: function() { - this.hide(); - ContentShareMod.DirectShare.show(ContentShareMod.currentContentData); - } - }, - - // Toolbar initialization and event bindings - initializeToolbars: function() { - const findEmptyButtons = (toolbar) => { - return Array.from(toolbar.querySelectorAll('.toolbar-btn')).filter( - btn => btn.textContent.trim() === "" - ); - }; - - const setupToolbar = (selector) => { - const toolbar = document.querySelector(selector); - if (!toolbar) return; - - const emptyButtons = findEmptyButtons(toolbar); - if (emptyButtons.length >= 2) { - emptyButtons[0].classList.add('content-share-btn'); - emptyButtons[0].textContent = 'Share'; - - emptyButtons[1].classList.add('content-unshare-btn'); - emptyButtons[1].textContent = 'Unshare'; - } - }; - - setupToolbar('#avatar-detail .avatar-toolbar'); - setupToolbar('#prop-detail .avatar-toolbar'); - }, - - bindEvents: function() { - // Avatar events - engine.on("LoadAvatarDetails", (avatarDetails) => { - const shareBtn = document.querySelector('#avatar-detail .content-share-btn'); - const unshareBtn = document.querySelector('#avatar-detail .content-unshare-btn'); - const canShareDirectly = avatarDetails.IsMine; - const canUnshare = avatarDetails.IsMine || avatarDetails.IsSharedWithMe; - - if (shareBtn) { - shareBtn.classList.remove('disabled'); - - if (canShareDirectly) { - shareBtn.onclick = () => ContentShareMod.ShareSelect.show(avatarDetails); - } else { - shareBtn.onclick = () => ContentShareMod.ShareBubble.show(avatarDetails); - } - } - - if (unshareBtn) { - if (canUnshare) { - unshareBtn.classList.remove('disabled'); - unshareBtn.onclick = () => { - if (avatarDetails.IsMine) { - ContentShareMod.Unshare.show(avatarDetails); - } else { - uiConfirmShow("Unshare Avatar", - "Are you sure you want to unshare this avatar?", - "unshare_avatar_confirmation", - avatarDetails.AvatarId); - } - }; - } else { - unshareBtn.classList.add('disabled'); - unshareBtn.onclick = null; - } - } - - ContentShareMod.currentContentData = avatarDetails; - }); - - // Prop events - engine.on("LoadPropDetails", (propDetails) => { - const shareBtn = document.querySelector('#prop-detail .content-share-btn'); - const unshareBtn = document.querySelector('#prop-detail .content-unshare-btn'); - const canShareDirectly = propDetails.IsMine; - const canUnshare = propDetails.IsMine || propDetails.IsSharedWithMe; - - if (shareBtn) { - shareBtn.classList.remove('disabled'); - - if (canShareDirectly) { - shareBtn.onclick = () => ContentShareMod.ShareSelect.show(propDetails); - } else { - shareBtn.onclick = () => ContentShareMod.ShareBubble.show(propDetails); - } - } - - if (unshareBtn) { - if (canUnshare) { - unshareBtn.classList.remove('disabled'); - unshareBtn.onclick = () => { - if (propDetails.IsMine) { - ContentShareMod.Unshare.show(propDetails); - } else { - uiConfirmShow("Unshare Prop", - "Are you sure you want to unshare this prop?", - "unshare_prop_confirmation", - propDetails.SpawnableId); - } - }; - } else { - unshareBtn.classList.add('disabled'); - unshareBtn.onclick = null; - } - } - - ContentShareMod.currentContentData = propDetails; - }); - - // Share response handlers - engine.on("OnHandleSharesResponse", (success, shares) => { - if (ContentShareMod.debugMode) { - console.log('Shares response:', success, shares); - } - ContentShareMod.Unshare.handleSharesResponse(success, shares); - }); - - engine.on("OnHandleRevokeResponse", (success, userId, error) => { - ContentShareMod.Unshare.handleRevokeResponse(success, userId, error); - }); - - engine.on("OnHandleShareResponse", function(success, userId, error) { - // Pass event to Unshare and DirectShare modules depending on which dialog is open - if (ContentShareMod.unshareDialog && !ContentShareMod.unshareDialog.classList.contains('hidden')) { - ContentShareMod.Unshare.handleShareResponse(success, userId, error); - } else if (ContentShareMod.directShareDialog && !ContentShareMod.directShareDialog.classList.contains('hidden')) { - ContentShareMod.DirectShare.handleShareResponse(success, userId, error); - } - }); - - // Share release handlers - engine.on("OnReleasedAvatarShare", (contentId) => { - if (!ContentShareMod.currentContentData || - ContentShareMod.currentContentData.AvatarId !== contentId) return; - - const unshareBtn = document.querySelector('#avatar-detail .content-unshare-btn'); - ContentShareMod.currentContentData.IsSharedWithMe = false; - - if (unshareBtn) { - unshareBtn.classList.add('disabled'); - unshareBtn.onclick = null; - } - - const contentIsAccessible = ContentShareMod.currentContentData.IsMine || - ContentShareMod.currentContentData.IsPublic; - - if (!contentIsAccessible) { - const detail = document.querySelector('#avatar-detail'); - if (detail) { - ['drop-btn', 'select-btn', 'fav-btn'].forEach(className => { - const button = detail.querySelector('.' + className); - if (button) { - button.classList.add('disabled'); - button.removeAttribute('onclick'); - } - }); - } - } - }); - - engine.on("OnReleasedPropShare", (contentId) => { - if (!ContentShareMod.currentContentData || - ContentShareMod.currentContentData.SpawnableId !== contentId) return; - - const unshareBtn = document.querySelector('#prop-detail .content-unshare-btn'); - ContentShareMod.currentContentData.IsSharedWithMe = false; - - if (unshareBtn) { - unshareBtn.classList.add('disabled'); - unshareBtn.onclick = null; - } - - const contentIsAccessible = ContentShareMod.currentContentData.IsMine || - ContentShareMod.currentContentData.IsPublic; - - if (!contentIsAccessible) { - const detail = document.querySelector('#prop-detail'); - if (detail) { - ['drop-btn', 'select-btn', 'fav-btn'].forEach(className => { - const button = detail.querySelector('.' + className); - if (button) { - button.classList.add('disabled'); - button.removeAttribute('onclick'); - } - }); - } - } - }); - - engine.on("OnHandleUsersResponse", (success, users, isInstanceUsers) => { - if (ContentShareMod.debugMode) { - console.log('Users response:', success, isInstanceUsers, users); - } - ContentShareMod.DirectShare.handleUsersResponse(success, users, isInstanceUsers); - }); - } -}; - -ContentShareMod.init(); - -"""; - - private const string UiConfirmId_ReleaseAvatarShareWarning = "unshare_avatar_confirmation"; - private const string UiConfirmId_ReleasePropShareWarning = "unshare_prop_confirmation"; - [HarmonyPostfix] - [HarmonyPatch(typeof(ViewManager), nameof(ViewManager.Start))] - public static void Postfix_ViewManager_Start(ViewManager __instance) + [HarmonyPatch(typeof(ViewManager), nameof(ViewManager.RegisterShareEvents))] + public static void Postfix_ViewManager_RegisterShareEvents(ViewManager __instance) { - // Inject the details toolbar patches when the game menu view is loaded - __instance.gameMenuView.Listener.FinishLoad += (_) => { - __instance.gameMenuView.View._view.ExecuteScript(DETAILS_TOOLBAR_PATCHES); - __instance.gameMenuView.View.BindCall("NAKCallShareContent", OnShareContent); - __instance.gameMenuView.View.BindCall("NAKGetContentShares", OnGetContentShares); - __instance.gameMenuView.View.BindCall("NAKRevokeContentShare", OnRevokeContentShare); - __instance.gameMenuView.View.BindCall("NAKCallShareContentDirect", OnShareContentDirect); - __instance.gameMenuView.View.BindCall("NAKGetUsersForSharing", OnGetUsersForSharing); + __instance.cohtmlView.View.BindCall("NAKCallShareContent", OnShareContent); + __instance.cohtmlView.Listener.FinishLoad += (_) => { + __instance.cohtmlView.View._view.ExecuteScript( +""" +(function waitForContentShare(){ + if (typeof ContentShare !== 'undefined') { + ContentShare.HasShareBubbles = true; + console.log('ShareBubbles patch applied'); + } else { + setTimeout(waitForContentShare, 50); + } +})(); +"""); }; - - // Add the event listener for the unshare confirmation dialog - __instance.OnUiConfirm.AddListener(OnReleaseContentShareConfirmation); - - return; + return; void OnShareContent( - string action, - string bubbleImpl, - string bubbleContent, - string shareRule, + string action, + string bubbleImpl, + string bubbleContent, + string shareRule, string shareLifetime, string shareAccess, string contentImage, @@ -1492,21 +96,21 @@ ContentShareMod.init(); // ShareRule: Public, FriendsOnly // ShareLifetime: TwoMinutes, Session // ShareAccess: PermanentAccess, SessionAccess, NoAccess - + ShareRule rule = shareRule switch { "Everyone" => ShareRule.Everyone, "FriendsOnly" => ShareRule.FriendsOnly, _ => ShareRule.Everyone }; - + ShareLifetime lifetime = shareLifetime switch { "Session" => ShareLifetime.Session, "TwoMinutes" => ShareLifetime.TwoMinutes, _ => ShareLifetime.TwoMinutes }; - + ShareAccess access = shareAccess switch { "Permanent" => ShareAccess.Permanent, @@ -1536,196 +140,9 @@ ContentShareMod.init(); ShareBubbleManager.Instance.SelectBubbleForPlace(contentImage, contentName, bubbleData); break; } - + // Close menu ViewManager.Instance.UiStateToggle(false); } - - void OnReleaseContentShareConfirmation(string id, string value, string contentId) - { - // Check if the confirmation event is for unsharing content - if (id != UiConfirmId_ReleaseAvatarShareWarning - && id != UiConfirmId_ReleasePropShareWarning) - return; - - //ShareBubblesMod.Logger.Msg($"Unshare confirmation received: {id}, {value}"); - - // Check if the user confirmed the unshare action - if (value != "true") - { - //ShareBubblesMod.Logger.Msg("Unshare action cancelled by user"); - return; - } - - //ShareBubblesMod.Logger.Msg("Releasing share..."); - - // Determine the content type based on the confirmation ID - ShareApiHelper.ShareContentType contentType = id == UiConfirmId_ReleaseAvatarShareWarning - ? ShareApiHelper.ShareContentType.Avatar - : ShareApiHelper.ShareContentType.Spawnable; - - Task.Run(async () => { - try - { - await ShareApiHelper.ReleaseShareAsync(contentType, contentId); - MTJobManager.RunOnMainThread("release_share_response", () => - { - // Cannot display a success message as opening details page pushes itself to top - // after talking to api, so success message would need to be timed to show after - // if (contentType == ApiShareHelper.ShareContentType.Avatar) - // ViewManager.Instance.RequestAvatarDetailsPage(contentId); - // else - // ViewManager.Instance.GetPropDetails(contentId); - - ViewManager.Instance.gameMenuView.View._view.TriggerEvent( - contentType == ShareApiHelper.ShareContentType.Avatar - ? "OnReleasedAvatarShare" : "OnReleasedPropShare", - contentId); - - ViewManager.Instance.TriggerPushNotification("Content unshared successfully", 3f); - }); - } - catch (ShareApiException ex) - { - ShareBubblesMod.Logger.Error($"Share API error: {ex.Message}"); - MTJobManager.RunOnMainThread("release_share_error", () => { - ViewManager.Instance.TriggerAlert("Release Share Error", ex.UserFriendlyMessage, -1, true); - }); - } - catch (Exception ex) - { - ShareBubblesMod.Logger.Error($"Unexpected error releasing share: {ex.Message}"); - MTJobManager.RunOnMainThread("release_share_error", () => { - ViewManager.Instance.TriggerAlert("Release Share Error", "An unexpected error occurred", -1, true); - }); - } - }); - } - - async void OnGetContentShares(string contentType, string contentId) - { - try - { - var response = await ShareApiHelper.GetSharesAsync>( - contentType == "Avatar" ? ShareApiHelper.ShareContentType.Avatar : ShareApiHelper.ShareContentType.Spawnable, - contentId - ); - - // TODO: somethign better than this cause this is ass and i need to replace the image urls with ImageCache coui ones - // FUICJK< - string json = JsonConvert.SerializeObject(response.Data); - - // log the json to console - //ShareBubblesMod.Logger.Msg($"Shares response: {json}"); - - __instance.gameMenuView.View.TriggerEvent("OnHandleSharesResponse", true, json); - } - catch (Exception ex) - { - ShareBubblesMod.Logger.Error($"Failed to get content shares: {ex.Message}"); - __instance.gameMenuView.View.TriggerEvent("OnHandleSharesResponse", false); - } - } - - async void OnRevokeContentShare(string contentType, string contentId, string userId) - { - try - { - await ShareApiHelper.ReleaseShareAsync( - contentType == "Avatar" ? ShareApiHelper.ShareContentType.Avatar : ShareApiHelper.ShareContentType.Spawnable, - contentId, - userId - ); - - __instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", true, userId); - } - catch (ShareApiException ex) - { - ShareBubblesMod.Logger.Error($"Share API error revoking share: {ex.Message}"); - __instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", false, userId, ex.UserFriendlyMessage); - } - catch (Exception ex) - { - ShareBubblesMod.Logger.Error($"Unexpected error revoking share: {ex.Message}"); - __instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", false, userId, "An unexpected error occurred"); - } - } - - async void OnShareContentDirect(string contentType, string contentId, string userId, string contentName = "", string contentImage = "") - { - try - { - ShareApiHelper.ShareContentType shareContentType = contentType == "Avatar" - ? ShareApiHelper.ShareContentType.Avatar - : ShareApiHelper.ShareContentType.Spawnable; - - await ShareApiHelper.ShareContentAsync( - shareContentType, - contentId, - userId - ); - - // Alert the user that the share occurred - //ModNetwork.SendDirectShareNotification(userId, shareContentType, contentId); - - __instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", true, userId); - } - catch (ShareApiException ex) - { - ShareBubblesMod.Logger.Error($"Share API error: {ex.Message}"); - __instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", false, userId, ex.UserFriendlyMessage); - } - catch (Exception ex) - { - ShareBubblesMod.Logger.Error($"Unexpected error sharing content: {ex.Message}"); - __instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", false, userId, "An unexpected error occurred"); - } - } - void OnGetUsersForSharing(string searchTerm = "") - { - try - { - if (!string.IsNullOrEmpty(searchTerm)) - { - // TODO: Search users implementation will go here - // For now just return an empty list - var response = new { entries = new List() }; - string json = JsonConvert.SerializeObject(response); - __instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", true, json, false); - } - else - { - // Get instance users - CVRPlayerManager playerManager = CVRPlayerManager.Instance; - if (playerManager == null) - { - __instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", false, false, true); - return; - } - - var response = new - { - entries = playerManager.NetworkPlayers - .Where(p => p != null && !string.IsNullOrEmpty(p.Uuid) - && !MetaPort.Instance.blockedUserIds.Contains(p.Uuid)) // You SHOULDNT HAVE TO DO THIS, but GS dumb - .Select(p => new - { - id = p.Uuid, - name = p.Username, - image = p.ApiProfileImageUrl - }) - .ToList() - }; - - string json = JsonConvert.SerializeObject(response); - __instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", true, json, true); - } - } - catch (Exception ex) - { - ShareBubblesMod.Logger.Error($"Failed to get users: {ex.Message}"); - __instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", false, false, string.IsNullOrEmpty(searchTerm)); - } - } } } \ No newline at end of file diff --git a/ShareBubbles/Properties/AssemblyInfo.cs b/ShareBubbles/Properties/AssemblyInfo.cs index ce8f183..13d5a60 100644 --- a/ShareBubbles/Properties/AssemblyInfo.cs +++ b/ShareBubbles/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ using NAK.ShareBubbles.Properties; downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ShareBubbles" )] -[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonGame("ChilloutVR", "ChilloutVR")] [assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] [assembly: MelonColor(255, 246, 25, 99)] // red-pink @@ -27,6 +27,6 @@ using NAK.ShareBubbles.Properties; namespace NAK.ShareBubbles.Properties; internal static class AssemblyInfoParams { - public const string Version = "1.0.5"; + public const string Version = "1.1.6"; public const string Author = "NotAKidoS, Exterrata, Noachi, RaidShadowLily, Tejler"; } \ No newline at end of file diff --git a/ShareBubbles/ShareBubbles/API/Exceptions/ShareApiExceptions.cs b/ShareBubbles/ShareBubbles/API/Exceptions/ShareApiExceptions.cs deleted file mode 100644 index 799a6a2..0000000 --- a/ShareBubbles/ShareBubbles/API/Exceptions/ShareApiExceptions.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Net; - -namespace NAK.ShareBubbles.API.Exceptions; - -public class ShareApiException : Exception -{ - public HttpStatusCode StatusCode { get; } // TODO: network back status code to claiming client, to show why the request failed - public string UserFriendlyMessage { get; } - - public ShareApiException(HttpStatusCode statusCode, string message, string userFriendlyMessage) - : base(message) - { - StatusCode = statusCode; - UserFriendlyMessage = userFriendlyMessage; - } -} - -public class ContentNotSharedException : ShareApiException -{ - public ContentNotSharedException(string contentId) - : base(HttpStatusCode.BadRequest, - $"Content {contentId} is not currently shared", - "This content is not currently shared with anyone") - { - } -} - -public class ContentNotFoundException : ShareApiException -{ - public ContentNotFoundException(string contentId) - : base(HttpStatusCode.NotFound, - $"Content {contentId} not found", - "The specified content could not be found") - { - } -} - -public class UserOnlyAllowsSharesFromFriendsException : ShareApiException -{ - public UserOnlyAllowsSharesFromFriendsException(string userId) - : base(HttpStatusCode.Forbidden, - $"User {userId} only accepts shares from friends", - "This user only accepts shares from friends") - { - } -} - -public class UserNotFoundException : ShareApiException -{ - public UserNotFoundException(string userId) - : base(HttpStatusCode.NotFound, - $"User {userId} not found", - "The specified user could not be found") - { - } -} - -public class ContentAlreadySharedException : ShareApiException -{ - public ContentAlreadySharedException(string contentId, string userId) - : base(HttpStatusCode.Conflict, - $"Content {contentId} is already shared with user {userId}", - "This content is already shared with this user") - { - } -} \ No newline at end of file diff --git a/ShareBubbles/ShareBubbles/API/PedestalInfoBatchProcessor.cs b/ShareBubbles/ShareBubbles/API/PedestalInfoBatchProcessor.cs index 967734b..43287cc 100644 --- a/ShareBubbles/ShareBubbles/API/PedestalInfoBatchProcessor.cs +++ b/ShareBubbles/ShareBubbles/API/PedestalInfoBatchProcessor.cs @@ -1,6 +1,5 @@ using ABI_RC.Core.Networking.API; using ABI_RC.Core.Networking.API.Responses; -using NAK.ShareBubbles.API.Responses; namespace NAK.ShareBubbles.API; @@ -34,6 +33,8 @@ public static class PedestalInfoBatchProcessor { PedestalType.Prop, false } }; + // This breaks compile accepting this change. + // ReSharper disable once ChangeFieldTypeToSystemThreadingLock private static readonly object _lock = new(); private const float BATCH_DELAY = 2f; diff --git a/ShareBubbles/ShareBubbles/API/Responses/ActiveSharesResponse.cs b/ShareBubbles/ShareBubbles/API/Responses/ActiveSharesResponse.cs deleted file mode 100644 index 70dc731..0000000 --- a/ShareBubbles/ShareBubbles/API/Responses/ActiveSharesResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace NAK.ShareBubbles.API.Responses; - -public class ActiveSharesResponse -{ - [JsonProperty("value")] - public List Value { get; set; } -} - -// Idk why not just reuse UserDetails -public class ShareUser -{ - [JsonProperty("image")] - public string Image { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } -} \ No newline at end of file diff --git a/ShareBubbles/ShareBubbles/API/ShareApiHelper.cs b/ShareBubbles/ShareBubbles/API/ShareApiHelper.cs deleted file mode 100644 index c338930..0000000 --- a/ShareBubbles/ShareBubbles/API/ShareApiHelper.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web; -using ABI_RC.Core.Networking; -using ABI_RC.Core.Networking.API; -using ABI_RC.Core.Networking.API.Responses; -using ABI_RC.Core.Savior; -using NAK.ShareBubbles.API.Exceptions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace NAK.ShareBubbles.API; - -/// -/// API for content sharing management. -/// -public static class ShareApiHelper -{ - #region Enums - - public enum ShareContentType - { - Avatar, - Spawnable - } - - private enum ShareApiOperation - { - ShareAvatar, - ReleaseAvatar, - - ShareSpawnable, - ReleaseSpawnable, - - GetAvatarShares, - GetSpawnableShares - } - - #endregion Enums - - #region Public API - - /// - /// Shares content with a specified user. - /// - /// Type of content to share - /// ID of the content - /// Target user ID - /// Response containing share information - /// Thrown when API request fails - /// Thrown when target user is not found - /// Thrown when content is not found - /// Thrown when user only accepts shares from friends - /// Thrown when content is already shared with user - public static Task> ShareContentAsync(ShareContentType type, string contentId, string userId) - { - ShareApiOperation operation = type == ShareContentType.Avatar - ? ShareApiOperation.ShareAvatar - : ShareApiOperation.ShareSpawnable; - - ShareRequest data = new() - { - ContentId = contentId, - UserId = userId - }; - - return MakeApiRequestAsync(operation, data); - } - - /// - /// Releases shared content from a specified user. - /// - /// Type of content to release - /// ID of the content - /// Optional user ID. If null, releases share from self - /// Response indicating success - /// Thrown when API request fails - /// Thrown when content is not shared - /// Thrown when content is not found - /// Thrown when specified user is not found - public static Task> ReleaseShareAsync(ShareContentType type, string contentId, string userId = null) - { - ShareApiOperation operation = type == ShareContentType.Avatar - ? ShareApiOperation.ReleaseAvatar - : ShareApiOperation.ReleaseSpawnable; - - // If no user ID is provided, release share from self - userId ??= MetaPort.Instance.ownerId; - - ShareRequest data = new() - { - ContentId = contentId, - UserId = userId - }; - - return MakeApiRequestAsync(operation, data); - } - - /// - /// Gets all shares for specified content. - /// - /// Type of content - /// ID of the content - /// Response containing share information - /// Thrown when API request fails - /// Thrown when content is not found - public static Task> GetSharesAsync(ShareContentType type, string contentId) - { - ShareApiOperation operation = type == ShareContentType.Avatar - ? ShareApiOperation.GetAvatarShares - : ShareApiOperation.GetSpawnableShares; - - ShareRequest data = new() { ContentId = contentId }; - return MakeApiRequestAsync(operation, data); - } - - #endregion Public API - - #region Private Implementation - - [Serializable] - private record ShareRequest - { - public string ContentId { get; set; } - public string UserId { get; set; } - } - - private static async Task> MakeApiRequestAsync(ShareApiOperation operation, ShareRequest data) - { - ValidateAuthenticationState(); - - (string endpoint, HttpMethod method) = GetApiEndpointAndMethod(operation, data); - using HttpRequestMessage request = CreateHttpRequest(endpoint, method, data); - - try - { - using HttpResponseMessage response = await ApiConnection._client.SendAsync(request); - string content = await response.Content.ReadAsStringAsync(); - - return HandleApiResponse(response, content, data.ContentId, data.UserId); - } - catch (HttpRequestException ex) - { - throw new ShareApiException( - HttpStatusCode.ServiceUnavailable, - $"Failed to communicate with the server: {ex.Message}", - "Unable to connect to the server. Please check your internet connection."); - } - catch (JsonException ex) - { - throw new ShareApiException( - HttpStatusCode.UnprocessableEntity, - $"Failed to process response data: {ex.Message}", - "Server returned invalid data. Please try again later."); - } - } - - private static void ValidateAuthenticationState() - { - if (!AuthManager.IsAuthenticated) - { - throw new ShareApiException( - HttpStatusCode.Unauthorized, - "User is not authenticated", - "Please log in to perform this action"); - } - } - - private static HttpRequestMessage CreateHttpRequest(string endpoint, HttpMethod method, ShareRequest data) - { - HttpRequestMessage request = new(method, endpoint); - - if (method == HttpMethod.Post) - { - JObject json = JObject.FromObject(data); - request.Content = new StringContent(json.ToString(), Encoding.UTF8, "application/json"); - } - - return request; - } - - private static BaseResponse HandleApiResponse(HttpResponseMessage response, string content, string contentId, string userId) - { - if (response.IsSuccessStatusCode) - return CreateSuccessResponse(content); - - // Let specific exceptions propagate up to the caller - throw response.StatusCode switch - { - HttpStatusCode.BadRequest => new ContentNotSharedException(contentId), - HttpStatusCode.NotFound when userId != null => new UserNotFoundException(userId), - HttpStatusCode.NotFound => new ContentNotFoundException(contentId), - HttpStatusCode.Forbidden => new UserOnlyAllowsSharesFromFriendsException(userId), - HttpStatusCode.Conflict => new ContentAlreadySharedException(contentId, userId), - _ => new ShareApiException( - response.StatusCode, - $"API request failed with status {response.StatusCode}: {content}", - "An unexpected error occurred. Please try again later.") - }; - } - - private static BaseResponse CreateSuccessResponse(string content) - { - var response = new BaseResponse("") - { - IsSuccessStatusCode = true, - HttpStatusCode = HttpStatusCode.OK - }; - - if (!string.IsNullOrEmpty(content)) - { - response.Data = JsonConvert.DeserializeObject(content); - } - - return response; - } - - private static (string endpoint, HttpMethod method) GetApiEndpointAndMethod(ShareApiOperation operation, ShareRequest data) - { - string baseUrl = $"{ApiConnection.APIAddress}/{ApiConnection.APIVersion}"; - string encodedContentId = HttpUtility.UrlEncode(data.ContentId); - - return operation switch - { - ShareApiOperation.GetAvatarShares => ($"{baseUrl}/avatars/{encodedContentId}/shares", HttpMethod.Get), - ShareApiOperation.ShareAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post), - ShareApiOperation.ReleaseAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete), - ShareApiOperation.GetSpawnableShares => ($"{baseUrl}/spawnables/{encodedContentId}/shares", HttpMethod.Get), - ShareApiOperation.ShareSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post), - ShareApiOperation.ReleaseSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete), - _ => throw new ArgumentException($"Unknown operation: {operation}") - }; - } - - #endregion Private Implementation -} \ No newline at end of file diff --git a/ShareBubbles/ShareBubbles/Implementation/AvatarBubbleImpl.cs b/ShareBubbles/ShareBubbles/Implementation/AvatarBubbleImpl.cs index b79f0c8..1c9e3de 100644 --- a/ShareBubbles/ShareBubbles/Implementation/AvatarBubbleImpl.cs +++ b/ShareBubbles/ShareBubbles/Implementation/AvatarBubbleImpl.cs @@ -1,10 +1,10 @@ using ABI_RC.Core.EventSystem; using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.IO; +using ABI_RC.Core.Networking.API; +using ABI_RC.Core.Networking.API.Exceptions; using ABI_RC.Core.Networking.API.UserWebsocket; -using ABI_RC.Core.Savior; using NAK.ShareBubbles.API; -using NAK.ShareBubbles.API.Exceptions; using ShareBubbles.ShareBubbles.Implementation; using UnityEngine; using Object = UnityEngine.Object; diff --git a/ShareBubbles/ShareBubbles/Implementation/SpawnableBubbleImpl.cs b/ShareBubbles/ShareBubbles/Implementation/SpawnableBubbleImpl.cs index 813222c..9de2b05 100644 --- a/ShareBubbles/ShareBubbles/Implementation/SpawnableBubbleImpl.cs +++ b/ShareBubbles/ShareBubbles/Implementation/SpawnableBubbleImpl.cs @@ -1,10 +1,10 @@ using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.IO; +using ABI_RC.Core.Networking.API; +using ABI_RC.Core.Networking.API.Exceptions; using ABI_RC.Core.Networking.API.UserWebsocket; using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; using NAK.ShareBubbles.API; -using NAK.ShareBubbles.API.Exceptions; using ShareBubbles.ShareBubbles.Implementation; using UnityEngine; using Object = UnityEngine.Object; diff --git a/ShareBubbles/ShareBubbles/Implementation/TempShareManager.cs b/ShareBubbles/ShareBubbles/Implementation/TempShareManager.cs index d35810a..f0425e5 100644 --- a/ShareBubbles/ShareBubbles/Implementation/TempShareManager.cs +++ b/ShareBubbles/ShareBubbles/Implementation/TempShareManager.cs @@ -1,8 +1,8 @@ -using ABI_RC.Core.Networking.API.UserWebsocket; +using ABI_RC.Core.Networking.API; +using ABI_RC.Core.Networking.API.UserWebsocket; using ABI_RC.Core.Networking.IO.Instancing; using ABI_RC.Core.Player; using ABI_RC.Systems.GameEventSystem; -using NAK.ShareBubbles.API; using Newtonsoft.Json; using UnityEngine; diff --git a/ShareBubbles/ShareBubbles/Networking/ModNetwork.Inbound.cs b/ShareBubbles/ShareBubbles/Networking/ModNetwork.Inbound.cs index a9caec4..cdf302c 100644 --- a/ShareBubbles/ShareBubbles/Networking/ModNetwork.Inbound.cs +++ b/ShareBubbles/ShareBubbles/Networking/ModNetwork.Inbound.cs @@ -1,7 +1,7 @@ -using ABI_RC.Core.Networking.IO.Social; +using ABI_RC.Core.Networking.API; +using ABI_RC.Core.Networking.IO.Social; using ABI_RC.Core.Savior; using ABI_RC.Systems.ModNetwork; -using NAK.ShareBubbles.API; using UnityEngine; namespace NAK.ShareBubbles.Networking; diff --git a/ShareBubbles/ShareBubbles/Networking/ModNetwork.Outbound.cs b/ShareBubbles/ShareBubbles/Networking/ModNetwork.Outbound.cs index a09e0e1..16b065c 100644 --- a/ShareBubbles/ShareBubbles/Networking/ModNetwork.Outbound.cs +++ b/ShareBubbles/ShareBubbles/Networking/ModNetwork.Outbound.cs @@ -1,5 +1,5 @@ -using ABI_RC.Systems.ModNetwork; -using NAK.ShareBubbles.API; +using ABI_RC.Core.Networking.API; +using ABI_RC.Systems.ModNetwork; using UnityEngine; namespace NAK.ShareBubbles.Networking; diff --git a/ShareBubbles/ShareBubbles/UI/BubbleInteract.cs b/ShareBubbles/ShareBubbles/UI/BubbleInteract.cs index ad2b4f4..ea5f369 100644 --- a/ShareBubbles/ShareBubbles/UI/BubbleInteract.cs +++ b/ShareBubbles/ShareBubbles/UI/BubbleInteract.cs @@ -1,6 +1,8 @@ using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.InteractionSystem.Base; using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using ABI_RC.Systems.InputManagement; using UnityEngine; namespace NAK.ShareBubbles.UI; @@ -9,11 +11,23 @@ namespace NAK.ShareBubbles.UI; // Must be added manually by ShareBubble creation... public class BubbleInteract : Interactable { - public override bool IsInteractableWithinRange(Vector3 sourcePos) + public override bool IsInteractable { - return Vector3.Distance(transform.position, sourcePos) < 1.5f; + get + { + if (ViewManager.Instance.IsAnyMenuOpen) + return true; + + if (!MetaPort.Instance.isUsingVr + && CVRInputManager.Instance.unlockMouse) + return true; + + return false; + } } + public override bool IsInteractableWithinRange(Vector3 sourcePos) => true; + public override void OnInteractDown(InteractionContext context, ControllerRay controllerRay) { // Not used diff --git a/ShareBubbles/format.json b/ShareBubbles/format.json index d2028d6..05b88e6 100644 --- a/ShareBubbles/format.json +++ b/ShareBubbles/format.json @@ -1,9 +1,9 @@ { "_id": 244, "name": "ShareBubbles", - "modversion": "1.0.5", - "gameversion": "2025r179", - "loaderversion": "0.6.1", + "modversion": "1.1.6", + "gameversion": "2025r80", + "loaderversion": "0.7.2", "modtype": "Mod", "author": "NotAKidoS, Exterrata, Noachi, RaidShadowLily, Tejler, Luc", "description": "Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network.\n### Features:\n- Can drop Share Bubbles linking to **any** Avatar or Prop page.\n - Share Bubbles can grant Permanent or Temporary shares to your **Private** Content if configured.\n- Adds basic in-game Share Management:\n - Directly share content to users within the current instance.\n - Revoke granted or received content shares.\n### Important:\nThe claiming of content shares through Share Bubbles will likely fail if you do not allow content shares from non-friends in your ABI Account settings!\n\n-# More information can be found on the [README](https://github.com/NotAKidoS/NAK_CVR_Mods/blob/main/ShareBubbles/README.md).", @@ -17,8 +17,8 @@ "requirements": [ "None" ], - "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ShareBubbles.dll", + "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/ShareBubbles.dll", "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ShareBubbles/", - "changelog": "- Fixes for 2025r179\n- Fixed Public/Private text on bubble not being correct\n- Fixed bubble displaying as locked or not claimed despite being shared the content if it was private and not owned by you", + "changelog": "- Fixes for 2025r180", "embedcolor": "#f61963" } \ No newline at end of file