managarten/apps-archived/memoro/apps/mobile/features/spaces/services/spaceService.ts
Till-JS 61d181fbc2 chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 07:03:59 +01:00

877 lines
24 KiB
TypeScript

import { useSpaceStore } from '../store/spaceStore';
// Types
export interface Memo {
id: string;
title: string;
user_id: string;
source?: any;
style?: any;
is_pinned?: boolean;
is_archived?: boolean;
is_public?: boolean;
metadata?: any;
created_at?: string;
updated_at?: string;
}
export interface SpaceInvite {
id: string;
email: string;
invitee_id?: string;
role: string;
status: string;
created_at: string;
responded_at?: string;
invited_by: string;
users?: {
email: string;
first_name?: string;
last_name?: string;
};
}
export interface Space {
id: string;
name: string;
description?: string;
memoCount?: number;
isDefault?: boolean;
color?: string;
created_at?: string;
updated_at?: string;
owner_id?: string;
app_id?: string;
credits?: number;
isOwner?: boolean; // Added flag to distinguish owners from regular members
roles?: {
members: {
[userId: string]: {
role: string;
added_at: string;
added_by: string;
};
};
};
apps?: {
name: string;
slug: string;
};
}
export interface CreateSpaceRequest {
name: string;
description?: string;
color?: string;
}
export interface UpdateSpaceRequest {
name: string;
description?: string;
color?: string;
}
class SpaceService {
private apiUrl: string;
constructor() {
this.apiUrl = process.env.EXPO_PUBLIC_MEMORO_MIDDLEWARE_URL || '';
}
/**
* Gets the currently selected space ID from the Zustand store
* @returns The current space ID or null if no space is selected
*/
getSpaceId(): string | null {
return useSpaceStore.getState().currentSpaceId;
}
/**
* Sets the currently selected space ID in the Zustand store
* @param spaceId The space ID to set as current, or null to clear selection
*/
setSpaceId(spaceId: string | null): void {
useSpaceStore.getState().setCurrentSpaceId(spaceId);
}
// Get all spaces for current user from the unified API
private async getAuthToken(): Promise<string | null> {
const { tokenManager } = await import('~/features/auth/services/tokenManager');
return tokenManager.getValidToken();
}
async getSpaces(): Promise<Space[]> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
// Call the memoro API endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Error fetching spaces: ${response.statusText}`);
}
const data = await response.json();
console.log(data);
// Transform the response to match our app's Space interface
return data.spaces.map((space: any) => ({
id: space.id,
name: space.name,
description: space.description || '',
memoCount: space.memo_count || 0,
isDefault: space.is_default || false,
color: space.color || '#4CAF50',
created_at: space.created_at,
updated_at: space.updated_at,
owner_id: space.owner_id,
app_id: space.app_id,
credits: space.credits || 0,
roles: space.roles,
isOwner: space.isOwner || false, // Include the isOwner flag from backend
}));
} catch (error) {
console.error('Failed to fetch spaces:', error);
throw error;
}
}
// Get a specific space by ID from the memoro API
async getSpace(spaceId: string): Promise<Space> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
// Call the memoro API endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Error fetching space: ${response.statusText}`);
}
const data = await response.json();
// Transform the response to match our app's Space interface
return {
id: data.space.id,
name: data.space.name,
description: data.space.description || '',
memoCount: data.space.memo_count || 0,
isDefault: data.space.is_default || false,
color: data.space.color || '#4CAF50',
created_at: data.space.created_at,
updated_at: data.space.updated_at,
owner_id: data.space.owner_id,
app_id: data.space.app_id,
credits: data.space.credits || 0,
roles: data.space.roles,
apps: data.space.apps,
isOwner: data.space.isOwner || false, // Include isOwner flag from backend
};
} catch (error) {
console.error(`Failed to fetch space ${spaceId}:`, error);
throw error;
}
}
// Create a new space with the memoro API
async createSpace(spaceData: CreateSpaceRequest): Promise<Space> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
// Prepare request data according to API docs
const requestData = {
name: spaceData.name,
};
// Call the memoro API endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Error creating space: ${response.statusText}`);
}
const data = await response.json();
console.log('Create space response:', JSON.stringify(data, null, 2));
// Handle both formats: {success, spaceId} or {space: {success, spaceId}}
const success = data.success || (data.space && data.space.success);
const spaceId = data.spaceId || (data.space && data.space.spaceId);
if (!success) {
throw new Error(data.error || (data.space && data.space.error) || 'Failed to create space');
}
if (!spaceId) {
throw new Error('No space ID returned from server');
}
try {
// Try to get the new space details
return await this.getSpace(spaceId);
} catch (fetchError) {
console.log('Could not fetch newly created space details. Creating fallback space object.');
// Create a fallback space object with minimal information
// The complete details will be loaded on next refresh
return {
id: spaceId,
name: spaceData.name,
description: '',
memoCount: 0,
isDefault: false,
color: spaceData.color || '#4CAF50',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
} catch (error) {
console.error('Failed to create space:', error);
throw error;
}
}
// Update an existing space with the memoro API
async updateSpace(spaceId: string, spaceData: UpdateSpaceRequest): Promise<Space> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
// Call the memoro API endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(spaceData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Error updating space: ${response.statusText}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to update space');
}
// Get the updated space details
return this.getSpace(spaceId);
} catch (error) {
console.error(`Failed to update space ${spaceId}:`, error);
throw error;
}
}
// Delete a space with the memoro API
async deleteSpace(spaceId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.log(`Deleting space at: ${this.apiUrl}/memoro/spaces/${spaceId}`);
// Call the memoro spaces API endpoint - following the established pattern
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error(`Error response status: ${response.status}`);
let errorMessage = `Error deleting space: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to delete space');
}
return true;
} catch (error) {
console.error(`Failed to delete space ${spaceId}:`, error);
throw error;
}
}
/**
* Leave a space (for non-owners)
* @param spaceId ID of the space to leave
* @returns Promise resolving to success status
*/
async leaveSpace(spaceId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.log(`Leaving space at: ${this.apiUrl}/memoro/spaces/${spaceId}/leave`);
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/leave`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error(`Error response status: ${response.status}`);
let errorMessage = `Error leaving space: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to leave space');
}
return true;
} catch (error) {
console.error(`Failed to leave space ${spaceId}:`, error);
throw error;
}
}
// Get all memos for a specific space
async getSpaceMemos(spaceId: string): Promise<Memo[]> {
// For development/testing, use mock data if API is not available or debug flag is set
const USE_MOCK_DATA = process.env.EXPO_PUBLIC_USE_MOCK_DATA === 'true';
if (USE_MOCK_DATA) {
console.debug('Using mock data for space memos');
// Return empty array for now - can be expanded with mock data if needed
return [];
}
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(
`Fetching memos for space ${spaceId} from ${this.apiUrl}/memoro/spaces/${spaceId}/memos`
);
// Call the memoro API endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/memos`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
let errorMessage = `Error fetching space memos: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
// If response is not JSON, use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
return data.memos || [];
} catch (error) {
console.error(`Failed to fetch memos for space ${spaceId}:`, error);
throw error;
}
}
// Link a memo to a space
async linkMemoToSpace(memoId: string, spaceId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.log(`Linking memo ${memoId} to space ${spaceId}`);
// Call the memoro API endpoint - corrected path
const response = await fetch(`${this.apiUrl}/memoro/link-memo`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ memoId, spaceId }),
});
if (!response.ok) {
console.error(`Error response status: ${response.status}`);
let errorMessage = `Error linking memo to space: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
return data.success;
} catch (error) {
console.error(`Failed to link memo ${memoId} to space ${spaceId}:`, error);
throw error;
}
}
// Unlink a memo from a space
async unlinkMemoFromSpace(memoId: string, spaceId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.log(`Unlinking memo ${memoId} from space ${spaceId}`);
// Call the memoro API endpoint - corrected path and method
const response = await fetch(`${this.apiUrl}/memoro/unlink-memo`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ memoId, spaceId }),
});
if (!response.ok) {
console.error(`Error response status: ${response.status}`);
let errorMessage = `Error unlinking memo from space: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
return data.success;
} catch (error) {
console.error(`Failed to unlink memo ${memoId} from space ${spaceId}:`, error);
throw error;
}
}
/**
* Invite a user to join a space
* @param spaceId ID of the space to invite the user to
* @param userEmail Email of the user to invite
* @param role Role to assign to the user (owner, admin, editor, viewer)
* @returns Promise with the invite ID or throws an error
*/
async inviteUser(spaceId: string, userEmail: string, role: string): Promise<string> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Inviting user ${userEmail} to space ${spaceId} with role ${role}`);
// Call the memoro/middleware API endpoint to add a space member
const response = await fetch(`${this.apiUrl}/api/spaces/members`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
spaceId,
userEmail,
role,
}),
});
if (!response.ok) {
let errorMessage = `Failed to invite user: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
if (!data.success || !data.inviteId) {
throw new Error(data.message || 'Failed to create invitation');
}
return data.inviteId;
} catch (error) {
console.error(`Failed to invite user to space ${spaceId}:`, error);
throw error;
}
}
/**
* Get all pending invites for a space
* @param spaceId ID of the space to get invites for
* @returns Promise with array of invites
*/
async getSpaceInvites(spaceId: string): Promise<SpaceInvite[]> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Fetching invites for space ${spaceId}`);
// Call the memoro service, which will proxy to mana-core-middleware if needed
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invites`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
let errorMessage = `Failed to get space invites: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
return data.invites || [];
} catch (error) {
console.error(`Failed to get invites for space ${spaceId}:`, error);
throw error;
}
}
/**
* Invite a user to a space by email
* @param spaceId ID of the space to invite to
* @param email Email of the user to invite
* @param role Role to assign (viewer, editor, admin, owner)
* @returns Promise resolving to the inviteId if successful
*/
async inviteUserToSpace(spaceId: string, email: string, role: string): Promise<string> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Inviting user ${email} to space ${spaceId} with role ${role}`);
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invite`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
role,
}),
});
if (!response.ok) {
let errorMessage = `Failed to invite user to space: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
return data.inviteId;
} catch (error) {
console.error(`Failed to invite user to space ${spaceId}:`, error);
throw error;
}
}
/**
* Resend a space invitation
* @param inviteId ID of the invitation to resend
* @returns Promise resolving to true if successful
*/
async resendInvite(inviteId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Resending invite ${inviteId}`);
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/${inviteId}/resend`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
let errorMessage = `Failed to resend invitation: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
return true;
} catch (error) {
console.error(`Failed to resend invite ${inviteId}:`, error);
throw error;
}
}
/**
* Accept a space invitation
* @param inviteId ID of the invitation to accept
* @returns Promise resolving to true if successful
*/
async acceptInvite(inviteId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Accepting invite ${inviteId}`);
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/accept`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inviteId,
}),
});
if (!response.ok) {
let errorMessage = `Failed to accept invitation: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
return true;
} catch (error) {
console.error(`Failed to accept invite ${inviteId}:`, error);
throw error;
}
}
/**
* Decline a space invitation
* @param inviteId ID of the invitation to decline
* @returns Promise resolving to true if successful
*/
async declineInvite(inviteId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Declining invite ${inviteId}`);
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/decline`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inviteId,
}),
});
if (!response.ok) {
let errorMessage = `Failed to decline invitation: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
return true;
} catch (error) {
console.error(`Failed to decline invite ${inviteId}:`, error);
throw error;
}
}
/**
* Cancel a space invitation (for space owners/admins)
* @param inviteId ID of the invitation to cancel
* @returns Promise resolving to true if successful
*/
async cancelInvite(inviteId: string): Promise<boolean> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug(`Canceling invite ${inviteId}`);
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/cancel`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inviteId,
}),
});
if (!response.ok) {
let errorMessage = `Failed to cancel invitation: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
return true;
} catch (error) {
console.error(`Failed to cancel invite ${inviteId}:`, error);
throw error;
}
}
/**
* Get all invitations for the current user
* @returns Promise with array of invites
*/
async getUserInvites(): Promise<SpaceInvite[]> {
try {
const token = await this.getAuthToken();
if (!token) {
throw new Error('Not authenticated');
}
console.debug('Getting user invites');
// Call the memoro service proxy endpoint
const response = await fetch(`${this.apiUrl}/memoro/invites/pending`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
let errorMessage = `Failed to get user invitations: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// If we can't parse JSON, just use the status text
}
throw new Error(errorMessage);
}
const data = await response.json();
console.log('User invites:', data);
return data.invites || [];
} catch (error) {
console.error('Failed to get user invites:', error);
throw error;
}
}
}
export const spaceService = new SpaceService();
export default spaceService;