feat: Implement Matrix Chat Client
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Users, FileText, Clock } from 'lucide-react'
|
||||
import { ArrowLeft, Users, FileText, Clock, MessageCircle } from 'lucide-react'
|
||||
import { api, CityRoom } from '@/lib/api'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { ChatRoom } from '@/components/chat/ChatRoom'
|
||||
import { MatrixChatRoom } from '@/components/chat/MatrixChatRoom'
|
||||
|
||||
// Force dynamic rendering - don't prerender at build time
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -73,12 +73,21 @@ export default async function RoomPage({ params }: PageProps) {
|
||||
{/* Chat Area */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="glass-panel h-[500px] sm:h-[600px] flex flex-col overflow-hidden">
|
||||
<ChatRoom
|
||||
roomId={room.id}
|
||||
roomSlug={room.slug}
|
||||
initialMessages={[]}
|
||||
/>
|
||||
<MatrixChatRoom roomSlug={room.slug} />
|
||||
</div>
|
||||
|
||||
{/* Matrix Room Info */}
|
||||
{room.matrix_room_id && (
|
||||
<div className="mt-4 glass-panel p-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MessageCircle className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-slate-400">Matrix Room:</span>
|
||||
<code className="text-xs font-mono text-cyan-400 bg-slate-800/50 px-2 py-0.5 rounded">
|
||||
{room.matrix_room_alias || room.matrix_room_id}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
|
||||
305
apps/web/src/components/chat/MatrixChatRoom.tsx
Normal file
305
apps/web/src/components/chat/MatrixChatRoom.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { MessageSquare, Wifi, WifiOff, Loader2, RefreshCw, AlertCircle } from 'lucide-react'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import { ChatInput } from './ChatInput'
|
||||
import { MatrixRestClient, createMatrixClient, ChatMessage as MatrixChatMessage } from '@/lib/matrix-client'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuth } from '@/context/AuthContext'
|
||||
|
||||
interface MatrixChatRoomProps {
|
||||
roomSlug: string
|
||||
}
|
||||
|
||||
type ConnectionStatus = 'loading' | 'connecting' | 'online' | 'error' | 'unauthenticated'
|
||||
|
||||
interface BootstrapData {
|
||||
matrix_hs_url: string
|
||||
matrix_user_id: string
|
||||
matrix_access_token: string
|
||||
matrix_device_id: string
|
||||
matrix_room_id: string
|
||||
matrix_room_alias: string
|
||||
room: {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function MatrixChatRoom({ roomSlug }: MatrixChatRoomProps) {
|
||||
const { user, token } = useAuth()
|
||||
const [messages, setMessages] = useState<MatrixChatMessage[]>([])
|
||||
const [status, setStatus] = useState<ConnectionStatus>('loading')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [bootstrap, setBootstrap] = useState<BootstrapData | null>(null)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const matrixClient = useRef<MatrixRestClient | null>(null)
|
||||
|
||||
// Scroll to bottom when new messages arrive
|
||||
const scrollToBottom = useCallback(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages, scrollToBottom])
|
||||
|
||||
// Initialize Matrix connection
|
||||
const initializeMatrix = useCallback(async () => {
|
||||
if (!token) {
|
||||
setStatus('unauthenticated')
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('loading')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// 1. Get bootstrap data
|
||||
const res = await fetch(`/api/city/chat/bootstrap?room_slug=${roomSlug}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || 'Failed to get chat bootstrap')
|
||||
}
|
||||
|
||||
const data: BootstrapData = await res.json()
|
||||
setBootstrap(data)
|
||||
|
||||
// 2. Create Matrix client
|
||||
setStatus('connecting')
|
||||
const client = createMatrixClient(data)
|
||||
matrixClient.current = client
|
||||
|
||||
// 3. Join room
|
||||
try {
|
||||
await client.joinRoom(data.matrix_room_id)
|
||||
} catch (e) {
|
||||
// Ignore join errors (might already be in room)
|
||||
console.log('Join room result:', e)
|
||||
}
|
||||
|
||||
// 4. Get initial messages
|
||||
const messagesRes = await client.getMessages(data.matrix_room_id, { limit: 50 })
|
||||
const initialMessages = messagesRes.chunk
|
||||
.filter(e => e.type === 'm.room.message' && e.content?.body)
|
||||
.map(e => client.mapToChatMessage(e))
|
||||
.reverse() // Oldest first
|
||||
|
||||
setMessages(initialMessages)
|
||||
|
||||
// 5. Start sync for real-time updates
|
||||
await client.initialSync()
|
||||
client.startSync((newMessage) => {
|
||||
setMessages(prev => {
|
||||
// Avoid duplicates
|
||||
if (prev.some(m => m.id === newMessage.id)) {
|
||||
return prev
|
||||
}
|
||||
return [...prev, newMessage]
|
||||
})
|
||||
})
|
||||
|
||||
setStatus('online')
|
||||
} catch (err) {
|
||||
console.error('Matrix initialization error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
setStatus('error')
|
||||
}
|
||||
}, [token, roomSlug])
|
||||
|
||||
useEffect(() => {
|
||||
initializeMatrix()
|
||||
|
||||
return () => {
|
||||
matrixClient.current?.stopSync()
|
||||
}
|
||||
}, [initializeMatrix])
|
||||
|
||||
const handleSendMessage = async (body: string) => {
|
||||
if (!matrixClient.current || !bootstrap) return
|
||||
|
||||
try {
|
||||
// Optimistically add message
|
||||
const tempId = `temp_${Date.now()}`
|
||||
const tempMessage: MatrixChatMessage = {
|
||||
id: tempId,
|
||||
senderId: bootstrap.matrix_user_id,
|
||||
senderName: 'You',
|
||||
text: body,
|
||||
timestamp: new Date(),
|
||||
isUser: true
|
||||
}
|
||||
setMessages(prev => [...prev, tempMessage])
|
||||
|
||||
// Send to Matrix
|
||||
const result = await matrixClient.current.sendMessage(bootstrap.matrix_room_id, body)
|
||||
|
||||
// Update temp message with real ID
|
||||
setMessages(prev => prev.map(m =>
|
||||
m.id === tempId ? { ...m, id: result.event_id } : m
|
||||
))
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err)
|
||||
// Remove failed message
|
||||
setMessages(prev => prev.filter(m => !m.id.startsWith('temp_')))
|
||||
setError('Не вдалося надіслати повідомлення')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
initializeMatrix()
|
||||
}
|
||||
|
||||
// Map MatrixChatMessage to legacy format for ChatMessage component
|
||||
const mapToLegacyFormat = (msg: MatrixChatMessage) => ({
|
||||
id: msg.id,
|
||||
room_id: bootstrap?.room.id || '',
|
||||
author_user_id: msg.isUser ? 'current_user' : msg.senderId,
|
||||
author_agent_id: null,
|
||||
body: msg.text,
|
||||
created_at: msg.timestamp.toISOString()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Connection status */}
|
||||
<div className="px-4 py-2 border-b border-white/10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm font-medium text-white">Matrix Chat</span>
|
||||
{bootstrap?.matrix_room_alias && (
|
||||
<span className="text-xs text-slate-500 font-mono">
|
||||
{bootstrap.matrix_room_alias}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
'flex items-center gap-1.5 px-2 py-1 rounded-full text-xs',
|
||||
status === 'loading' && 'bg-slate-500/20 text-slate-400',
|
||||
status === 'connecting' && 'bg-amber-500/20 text-amber-400',
|
||||
status === 'online' && 'bg-emerald-500/20 text-emerald-400',
|
||||
status === 'error' && 'bg-red-500/20 text-red-400',
|
||||
status === 'unauthenticated' && 'bg-amber-500/20 text-amber-400'
|
||||
)}>
|
||||
{status === 'loading' && (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span>Завантаження...</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'connecting' && (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span>Підключення до Matrix...</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'online' && (
|
||||
<>
|
||||
<Wifi className="w-3 h-3" />
|
||||
<span>Онлайн</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<WifiOff className="w-3 h-3" />
|
||||
<span>Помилка</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'unauthenticated' && (
|
||||
<>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
<span>Потрібен вхід</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error / Auth required message */}
|
||||
{(status === 'error' || status === 'unauthenticated') && (
|
||||
<div className="px-4 py-3 bg-red-500/10 border-b border-red-500/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-red-400">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>
|
||||
{status === 'unauthenticated'
|
||||
? 'Увійдіть, щоб приєднатися до чату'
|
||||
: error || 'Помилка підключення'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{status === 'error' && (
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="flex items-center gap-1 px-3 py-1 text-xs bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-full transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
Повторити
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages area */}
|
||||
<div className="flex-1 overflow-y-auto py-4 space-y-1">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
||||
<div className="w-16 h-16 mb-4 rounded-full bg-slate-800/50 flex items-center justify-center">
|
||||
<MessageSquare className="w-8 h-8 text-slate-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
{status === 'online'
|
||||
? 'Поки що немає повідомлень'
|
||||
: status === 'unauthenticated'
|
||||
? 'Увійдіть для доступу до чату'
|
||||
: 'Підключення до Matrix...'
|
||||
}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 max-w-sm">
|
||||
{status === 'online'
|
||||
? 'Будьте першим, хто напише в цій кімнаті! Ваше повідомлення синхронізується з Matrix.'
|
||||
: status === 'unauthenticated'
|
||||
? 'Для участі в чаті потрібна авторизація'
|
||||
: 'Встановлюємо зʼєднання з Matrix сервером...'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{messages.map((message) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={mapToLegacyFormat(message)}
|
||||
isOwn={message.isUser}
|
||||
/>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={status !== 'online'}
|
||||
placeholder={
|
||||
status === 'online'
|
||||
? 'Напишіть повідомлення...'
|
||||
: status === 'unauthenticated'
|
||||
? 'Увійдіть для надсилання повідомлень'
|
||||
: 'Очікування підключення...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface CityRoom {
|
||||
created_by: string | null
|
||||
members_online: number
|
||||
last_event: string | null
|
||||
// Matrix integration
|
||||
matrix_room_id: string | null
|
||||
matrix_room_alias: string | null
|
||||
}
|
||||
|
||||
export interface SecondMeProfile {
|
||||
|
||||
315
apps/web/src/lib/matrix-client.ts
Normal file
315
apps/web/src/lib/matrix-client.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Lightweight Matrix REST Client for DAARION
|
||||
*
|
||||
* Uses Matrix Client-Server API directly without heavy SDK
|
||||
*/
|
||||
|
||||
export interface MatrixClientConfig {
|
||||
baseUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
export interface MatrixMessage {
|
||||
event_id: string;
|
||||
sender: string;
|
||||
origin_server_ts: number;
|
||||
content: {
|
||||
msgtype: string;
|
||||
body: string;
|
||||
format?: string;
|
||||
formatted_body?: string;
|
||||
};
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface MatrixMessagesResponse {
|
||||
chunk: MatrixMessage[];
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export interface MatrixSyncResponse {
|
||||
next_batch: string;
|
||||
rooms?: {
|
||||
join?: {
|
||||
[roomId: string]: {
|
||||
timeline?: {
|
||||
events: MatrixMessage[];
|
||||
prev_batch?: string;
|
||||
};
|
||||
state?: {
|
||||
events: any[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
isUser: boolean;
|
||||
}
|
||||
|
||||
export class MatrixRestClient {
|
||||
private baseUrl: string;
|
||||
private accessToken: string;
|
||||
private userId: string;
|
||||
private roomId: string | null = null;
|
||||
private syncToken: string | null = null;
|
||||
private syncAbortController: AbortController | null = null;
|
||||
private onMessageCallback: ((message: ChatMessage) => void) | null = null;
|
||||
private isSyncing: boolean = false;
|
||||
|
||||
constructor(config: MatrixClientConfig) {
|
||||
this.baseUrl = config.baseUrl;
|
||||
this.accessToken = config.accessToken;
|
||||
this.userId = config.userId;
|
||||
this.roomId = config.roomId || null;
|
||||
}
|
||||
|
||||
private authHeaders(): HeadersInit {
|
||||
return {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a Matrix room
|
||||
*/
|
||||
async joinRoom(roomId: string): Promise<{ room_id: string }> {
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/_matrix/client/v3/join/${encodeURIComponent(roomId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this.authHeaders()
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
// M_FORBIDDEN means already joined or not allowed
|
||||
if (error.errcode !== 'M_FORBIDDEN') {
|
||||
throw new Error(error.error || 'Failed to join room');
|
||||
}
|
||||
}
|
||||
|
||||
this.roomId = roomId;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages from a room
|
||||
*/
|
||||
async getMessages(roomId: string, options?: { limit?: number; from?: string; dir?: 'b' | 'f' }): Promise<MatrixMessagesResponse> {
|
||||
const params = new URLSearchParams({
|
||||
dir: options?.dir || 'b',
|
||||
limit: String(options?.limit || 50),
|
||||
filter: JSON.stringify({ types: ['m.room.message'] })
|
||||
});
|
||||
|
||||
if (options?.from) {
|
||||
params.set('from', options.from);
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages?${params}`,
|
||||
{ headers: this.authHeaders() }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Failed to get messages');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text message to a room
|
||||
*/
|
||||
async sendMessage(roomId: string, body: string): Promise<{ event_id: string }> {
|
||||
const txnId = `m${Date.now()}.${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: this.authHeaders(),
|
||||
body: JSON.stringify({
|
||||
msgtype: 'm.text',
|
||||
body: body
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Failed to send message');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initial sync to get sync token
|
||||
*/
|
||||
async initialSync(): Promise<MatrixSyncResponse> {
|
||||
const params = new URLSearchParams({
|
||||
timeout: '0',
|
||||
filter: JSON.stringify({
|
||||
room: {
|
||||
timeline: { limit: 1 },
|
||||
state: { lazy_load_members: true }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/_matrix/client/v3/sync?${params}`,
|
||||
{ headers: this.authHeaders() }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Failed to sync');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
this.syncToken = data.next_batch;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start long-polling for new messages
|
||||
*/
|
||||
startSync(onMessage: (message: ChatMessage) => void): void {
|
||||
this.onMessageCallback = onMessage;
|
||||
this.isSyncing = true;
|
||||
this.syncLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop syncing
|
||||
*/
|
||||
stopSync(): void {
|
||||
this.isSyncing = false;
|
||||
if (this.syncAbortController) {
|
||||
this.syncAbortController.abort();
|
||||
this.syncAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async syncLoop(): Promise<void> {
|
||||
while (this.isSyncing) {
|
||||
try {
|
||||
this.syncAbortController = new AbortController();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
timeout: '30000',
|
||||
filter: JSON.stringify({
|
||||
room: {
|
||||
timeline: { limit: 50 },
|
||||
state: { lazy_load_members: true }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (this.syncToken) {
|
||||
params.set('since', this.syncToken);
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}/_matrix/client/v3/sync?${params}`,
|
||||
{
|
||||
headers: this.authHeaders(),
|
||||
signal: this.syncAbortController.signal
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('Sync failed:', await res.text());
|
||||
// Wait before retry
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const data: MatrixSyncResponse = await res.json();
|
||||
this.syncToken = data.next_batch;
|
||||
|
||||
// Process new messages
|
||||
if (data.rooms?.join && this.roomId) {
|
||||
const roomData = data.rooms.join[this.roomId];
|
||||
if (roomData?.timeline?.events) {
|
||||
for (const event of roomData.timeline.events) {
|
||||
if (event.type === 'm.room.message' && event.content?.body) {
|
||||
const chatMessage = this.mapToChatMessage(event);
|
||||
this.onMessageCallback?.(chatMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// Sync was stopped
|
||||
break;
|
||||
}
|
||||
console.error('Sync error:', error);
|
||||
// Wait before retry
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Matrix event to ChatMessage
|
||||
*/
|
||||
mapToChatMessage(event: MatrixMessage): ChatMessage {
|
||||
// Extract display name from Matrix user ID
|
||||
// @daarion_abc123:daarion.space -> User abc123
|
||||
const senderName = event.sender
|
||||
.split(':')[0]
|
||||
.replace('@daarion_', 'User ')
|
||||
.replace('@', '');
|
||||
|
||||
return {
|
||||
id: event.event_id,
|
||||
senderId: event.sender,
|
||||
senderName: senderName,
|
||||
text: event.content.body,
|
||||
timestamp: new Date(event.origin_server_ts),
|
||||
isUser: event.sender === this.userId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user ID
|
||||
*/
|
||||
getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Matrix client from bootstrap data
|
||||
*/
|
||||
export function createMatrixClient(bootstrap: {
|
||||
matrix_hs_url: string;
|
||||
matrix_user_id: string;
|
||||
matrix_access_token: string;
|
||||
matrix_room_id: string;
|
||||
}): MatrixRestClient {
|
||||
return new MatrixRestClient({
|
||||
baseUrl: bootstrap.matrix_hs_url,
|
||||
accessToken: bootstrap.matrix_access_token,
|
||||
userId: bootstrap.matrix_user_id,
|
||||
roomId: bootstrap.matrix_room_id
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user