improve(messages): add group roles (admin/member) with role management UI
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- New PATCH /api/conversations/<id>/members/<uid> endpoint for role changes - Owner can promote members to admin and demote back to member - Admin can add/remove members and edit group name (same as owner except role changes) - Member list shows role labels (Właściciel/Administrator/Członek) - Fix: state.currentConversation → state.currentConversationId (panel was empty) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b626e4b76d
commit
195abb0be4
@ -471,7 +471,7 @@ def api_conversation_detail(conv_id):
|
||||
@login_required
|
||||
@member_required
|
||||
def api_conversation_edit(conv_id):
|
||||
"""Edit conversation (name, description). Owner only, groups only."""
|
||||
"""Edit conversation (name, description). Owner or admin only, groups only."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
conv = db.query(Conversation).filter_by(id=conv_id).first()
|
||||
@ -479,8 +479,12 @@ def api_conversation_edit(conv_id):
|
||||
return jsonify({'error': 'Konwersacja nie istnieje'}), 404
|
||||
if not conv.is_group:
|
||||
return jsonify({'error': 'Nie można edytować konwersacji 1:1'}), 400
|
||||
if conv.owner_id != current_user.id:
|
||||
return jsonify({'error': 'Tylko właściciel może edytować konwersację'}), 403
|
||||
|
||||
caller_membership = db.query(ConversationMember).filter_by(
|
||||
conversation_id=conv_id, user_id=current_user.id
|
||||
).first()
|
||||
if not caller_membership or caller_membership.role not in ('owner', 'admin'):
|
||||
return jsonify({'error': 'Tylko właściciel lub administrator może edytować'}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
@ -543,7 +547,7 @@ def api_conversation_delete(conv_id):
|
||||
@login_required
|
||||
@member_required
|
||||
def api_conversation_add_member(conv_id):
|
||||
"""Add a member to a group conversation. Owner only."""
|
||||
"""Add a member to a group conversation. Owner or admin only."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
conv = db.query(Conversation).filter_by(id=conv_id).first()
|
||||
@ -551,8 +555,12 @@ def api_conversation_add_member(conv_id):
|
||||
return jsonify({'error': 'Konwersacja nie istnieje'}), 404
|
||||
if not conv.is_group:
|
||||
return jsonify({'error': 'Nie można dodać osób do konwersacji 1:1'}), 400
|
||||
if conv.owner_id != current_user.id:
|
||||
return jsonify({'error': 'Tylko właściciel może dodawać członków'}), 403
|
||||
|
||||
caller_membership = db.query(ConversationMember).filter_by(
|
||||
conversation_id=conv_id, user_id=current_user.id
|
||||
).first()
|
||||
if not caller_membership or caller_membership.role not in ('owner', 'admin'):
|
||||
return jsonify({'error': 'Tylko właściciel lub administrator może dodawać członków'}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or 'user_id' not in data:
|
||||
@ -631,7 +639,7 @@ def api_conversation_add_member(conv_id):
|
||||
@login_required
|
||||
@member_required
|
||||
def api_conversation_remove_member(conv_id, user_id):
|
||||
"""Remove member from conversation. Owner can remove anyone, member can leave."""
|
||||
"""Remove member from conversation. Owner/admin can remove others, anyone can leave."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
conv = db.query(Conversation).filter_by(id=conv_id).first()
|
||||
@ -639,12 +647,22 @@ def api_conversation_remove_member(conv_id, user_id):
|
||||
return jsonify({'error': 'Konwersacja nie istnieje'}), 404
|
||||
|
||||
# Permission check
|
||||
is_owner = conv.owner_id == current_user.id
|
||||
caller_membership = db.query(ConversationMember).filter_by(
|
||||
conversation_id=conv_id, user_id=current_user.id
|
||||
).first()
|
||||
if not caller_membership:
|
||||
return jsonify({'error': 'Brak dostępu do konwersacji'}), 403
|
||||
|
||||
is_admin_or_owner = caller_membership.role in ('owner', 'admin')
|
||||
is_self = user_id == current_user.id
|
||||
|
||||
if not is_owner and not is_self:
|
||||
if not is_admin_or_owner and not is_self:
|
||||
return jsonify({'error': 'Brak uprawnień'}), 403
|
||||
|
||||
# Cannot remove owner
|
||||
if user_id == conv.owner_id and not is_self:
|
||||
return jsonify({'error': 'Nie można usunąć właściciela grupy'}), 403
|
||||
|
||||
member = db.query(ConversationMember).filter_by(
|
||||
conversation_id=conv_id, user_id=user_id
|
||||
).first()
|
||||
@ -670,6 +688,53 @@ def api_conversation_remove_member(conv_id, user_id):
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 8b. PATCH /api/conversations/<id>/members/<user_id> — Change role
|
||||
# ============================================================
|
||||
|
||||
@bp.route('/api/conversations/<int:conv_id>/members/<int:user_id>', methods=['PATCH'])
|
||||
@login_required
|
||||
@member_required
|
||||
def api_conversation_change_role(conv_id, user_id):
|
||||
"""Change member role in group conversation. Owner only."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
conv = db.query(Conversation).filter_by(id=conv_id).first()
|
||||
if not conv:
|
||||
return jsonify({'error': 'Konwersacja nie istnieje'}), 404
|
||||
|
||||
if conv.owner_id != current_user.id:
|
||||
return jsonify({'error': 'Tylko właściciel może zmieniać role'}), 403
|
||||
|
||||
if user_id == current_user.id:
|
||||
return jsonify({'error': 'Nie możesz zmienić swojej własnej roli'}), 400
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or 'role' not in data:
|
||||
return jsonify({'error': 'Podaj role'}), 400
|
||||
|
||||
new_role = data['role']
|
||||
if new_role not in ('admin', 'member'):
|
||||
return jsonify({'error': 'Rola musi być "admin" lub "member"'}), 400
|
||||
|
||||
member = db.query(ConversationMember).filter_by(
|
||||
conversation_id=conv_id, user_id=user_id
|
||||
).first()
|
||||
if not member:
|
||||
return jsonify({'error': 'Użytkownik nie jest członkiem konwersacji'}), 404
|
||||
|
||||
member.role = new_role
|
||||
db.commit()
|
||||
|
||||
return jsonify({'ok': True, 'role': new_role})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error changing role in conversation {conv_id}: {e}")
|
||||
return jsonify({'error': 'Błąd serwera'}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 9. PATCH /api/conversations/<id>/settings — User settings
|
||||
# ============================================================
|
||||
|
||||
@ -188,7 +188,7 @@
|
||||
renderItem: function (conv) {
|
||||
var item = el('div', 'conversation-item');
|
||||
item.dataset.id = conv.id;
|
||||
if (conv.id === state.currentConversationId) item.classList.add('active');
|
||||
if (conv.id === state.currentConversationIdId) item.classList.add('active');
|
||||
if (conv.unread_count > 0) item.classList.add('unread');
|
||||
|
||||
// Avatar
|
||||
@ -262,7 +262,7 @@
|
||||
},
|
||||
|
||||
selectConversation: function (id) {
|
||||
state.currentConversationId = id;
|
||||
state.currentConversationIdId = id;
|
||||
|
||||
// Update active class
|
||||
document.querySelectorAll('.conversation-item').forEach(function (el) {
|
||||
@ -514,7 +514,7 @@
|
||||
|
||||
// Sender name (groups, other people's messages)
|
||||
if (!isMine && msg.sender) {
|
||||
var conv = state.conversations.find(function (c) { return c.id === state.currentConversationId; });
|
||||
var conv = state.conversations.find(function (c) { return c.id === state.currentConversationIdId; });
|
||||
if (conv && conv.is_group) {
|
||||
var senderName = el('div', 'message-subject', msg.sender.name);
|
||||
bubble.appendChild(senderName);
|
||||
@ -636,7 +636,7 @@
|
||||
var check = el('span', 'read-status');
|
||||
|
||||
// Check if other members have read this message
|
||||
var details = state.conversationDetails[state.currentConversationId];
|
||||
var details = state.conversationDetails[state.currentConversationIdId];
|
||||
var isRead = false;
|
||||
var readAt = null;
|
||||
|
||||
@ -663,7 +663,7 @@
|
||||
var otherMembers = details.members.filter(function (m) {
|
||||
return m.user_id !== window.__CURRENT_USER__.id;
|
||||
});
|
||||
var conv = state.conversations.find(function (c) { return c.id === state.currentConversationId; });
|
||||
var conv = state.conversations.find(function (c) { return c.id === state.currentConversationIdId; });
|
||||
var isGroup = conv && conv.is_group;
|
||||
|
||||
if (isGroup) {
|
||||
@ -761,7 +761,7 @@
|
||||
if (dominated) return;
|
||||
state.messages[convId].push(msg);
|
||||
|
||||
if (convId !== state.currentConversationId) return;
|
||||
if (convId !== state.currentConversationIdId) return;
|
||||
|
||||
var container = document.getElementById('chatMessages');
|
||||
if (!container) return;
|
||||
@ -1011,7 +1011,7 @@
|
||||
list.style.overflowY = 'auto';
|
||||
|
||||
state.conversations.forEach(function (conv) {
|
||||
if (conv.id === state.currentConversationId) return;
|
||||
if (conv.id === state.currentConversationIdId) return;
|
||||
var item = el('div', 'conversation-item');
|
||||
item.style.cursor = 'pointer';
|
||||
|
||||
@ -1064,7 +1064,7 @@
|
||||
} catch (_) {}
|
||||
}
|
||||
// Refresh pins
|
||||
ChatView.loadConversationDetails(state.currentConversationId);
|
||||
ChatView.loadConversationDetails(state.currentConversationIdId);
|
||||
},
|
||||
|
||||
startEdit: function (msg) {
|
||||
@ -1122,7 +1122,7 @@
|
||||
api('/api/messages/' + msg.id, 'PATCH', { content: newContent })
|
||||
.then(function (updated) {
|
||||
// Update in state
|
||||
var msgs = state.messages[state.currentConversationId] || [];
|
||||
var msgs = state.messages[state.currentConversationIdId] || [];
|
||||
var idx = msgs.findIndex(function (m) { return m.id === msg.id; });
|
||||
if (idx !== -1) msgs[idx] = updated;
|
||||
state.editingMessageId = null;
|
||||
@ -1136,7 +1136,7 @@
|
||||
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
state.editingMessageId = null;
|
||||
ChatView.renderMessages(state.messages[state.currentConversationId] || []);
|
||||
ChatView.renderMessages(state.messages[state.currentConversationIdId] || []);
|
||||
ChatView.scrollToMessage(msg.id);
|
||||
});
|
||||
|
||||
@ -1162,7 +1162,7 @@
|
||||
api('/api/messages/' + msg.id, 'DELETE')
|
||||
.then(function () {
|
||||
// Update in state
|
||||
var msgs = state.messages[state.currentConversationId] || [];
|
||||
var msgs = state.messages[state.currentConversationIdId] || [];
|
||||
var idx = msgs.findIndex(function (m) { return m.id === msg.id; });
|
||||
if (idx !== -1) {
|
||||
msgs[idx].is_deleted = true;
|
||||
@ -1207,7 +1207,7 @@
|
||||
await api('/api/messages/' + messageId + '/reactions', 'POST', { emoji: emoji });
|
||||
}
|
||||
// Refresh messages to update reactions display
|
||||
ChatView.loadMessages(state.currentConversationId);
|
||||
ChatView.loadMessages(state.currentConversationIdId);
|
||||
} catch (e) {
|
||||
// silently ignore
|
||||
}
|
||||
@ -1216,7 +1216,7 @@
|
||||
addFromPicker: async function (messageId, emoji) {
|
||||
try {
|
||||
await api('/api/messages/' + messageId + '/reactions', 'POST', { emoji: emoji });
|
||||
ChatView.loadMessages(state.currentConversationId);
|
||||
ChatView.loadMessages(state.currentConversationIdId);
|
||||
} catch (e) {
|
||||
// silently ignore
|
||||
}
|
||||
@ -1363,11 +1363,11 @@
|
||||
},
|
||||
|
||||
sendContent: function (html, text) {
|
||||
if (!state.currentConversationId) return;
|
||||
if (!state.currentConversationIdId) return;
|
||||
// Add to queue with current state snapshot
|
||||
var tempId = 'temp-' + Date.now() + '-' + Math.random();
|
||||
Composer._sendQueue.push({
|
||||
convId: state.currentConversationId,
|
||||
convId: state.currentConversationIdId,
|
||||
html: html,
|
||||
text: text,
|
||||
replyTo: state.replyToMessage,
|
||||
@ -1382,7 +1382,7 @@
|
||||
// Show optimistic message immediately
|
||||
ChatView.appendMessage({
|
||||
id: tempId,
|
||||
conversation_id: state.currentConversationId,
|
||||
conversation_id: state.currentConversationIdId,
|
||||
content: html,
|
||||
sender_id: window.__CURRENT_USER__ ? window.__CURRENT_USER__.id : null,
|
||||
sender: window.__CURRENT_USER__ || {},
|
||||
@ -1405,7 +1405,7 @@
|
||||
|
||||
// send: called from button click — reads from Quill
|
||||
send: async function () {
|
||||
if (!state.currentConversationId || !state.quill) return;
|
||||
if (!state.currentConversationIdId || !state.quill) return;
|
||||
|
||||
var html = state.quill.root.innerHTML;
|
||||
var text = state.quill.getText().trim();
|
||||
@ -1414,7 +1414,7 @@
|
||||
if (!text && !hasImage && !state.attachedFiles.length) return;
|
||||
if (!text && hasImage) text = '📷';
|
||||
|
||||
var convId = state.currentConversationId;
|
||||
var convId = state.currentConversationIdId;
|
||||
var savedReplyTo = state.replyToMessage;
|
||||
var savedFiles = state.attachedFiles.slice();
|
||||
|
||||
@ -1532,17 +1532,17 @@
|
||||
},
|
||||
|
||||
sendTyping: function () {
|
||||
if (!state.currentConversationId) return;
|
||||
if (!state.currentConversationIdId) return;
|
||||
if (state.typingTimeout) clearTimeout(state.typingTimeout);
|
||||
state.typingTimeout = setTimeout(function () {
|
||||
api('/api/conversations/' + state.currentConversationId + '/typing', 'POST')
|
||||
api('/api/conversations/' + state.currentConversationIdId + '/typing', 'POST')
|
||||
.catch(function () {});
|
||||
}, 300);
|
||||
// Debounce: only send after 300ms of no typing, then don't send again for 2s
|
||||
clearTimeout(state.typingTimeout);
|
||||
if (!state._lastTypingSent || Date.now() - state._lastTypingSent > 2000) {
|
||||
state._lastTypingSent = Date.now();
|
||||
api('/api/conversations/' + state.currentConversationId + '/typing', 'POST')
|
||||
api('/api/conversations/' + state.currentConversationIdId + '/typing', 'POST')
|
||||
.catch(function () {});
|
||||
}
|
||||
},
|
||||
@ -1616,8 +1616,8 @@
|
||||
state.sse.addEventListener('message_pinned', function (e) {
|
||||
try {
|
||||
var data = JSON.parse(e.data);
|
||||
if (data.conversation_id === state.currentConversationId) {
|
||||
ChatView.loadConversationDetails(state.currentConversationId);
|
||||
if (data.conversation_id === state.currentConversationIdId) {
|
||||
ChatView.loadConversationDetails(state.currentConversationIdId);
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
@ -1625,8 +1625,8 @@
|
||||
state.sse.addEventListener('message_unpinned', function (e) {
|
||||
try {
|
||||
var data = JSON.parse(e.data);
|
||||
if (data.conversation_id === state.currentConversationId) {
|
||||
ChatView.loadConversationDetails(state.currentConversationId);
|
||||
if (data.conversation_id === state.currentConversationIdId) {
|
||||
ChatView.loadConversationDetails(state.currentConversationIdId);
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
@ -1652,7 +1652,7 @@
|
||||
var convId = msg.conversation_id;
|
||||
|
||||
// Append to current view if active
|
||||
if (convId === state.currentConversationId) {
|
||||
if (convId === state.currentConversationIdId) {
|
||||
ChatView.appendMessage(msg);
|
||||
// Mark read
|
||||
api('/api/conversations/' + convId + '/read', 'POST').catch(function () {});
|
||||
@ -1670,7 +1670,7 @@
|
||||
created_at: msg.created_at,
|
||||
},
|
||||
updated_at: msg.created_at,
|
||||
unread_count: convId === state.currentConversationId
|
||||
unread_count: convId === state.currentConversationIdId
|
||||
? 0
|
||||
: (conv.unread_count || 0) + 1,
|
||||
});
|
||||
@ -1685,8 +1685,8 @@
|
||||
|
||||
handleMessageRead: function (data) {
|
||||
// Update read receipts if viewing the same conversation
|
||||
if (data.conversation_id === state.currentConversationId) {
|
||||
var details = state.conversationDetails[state.currentConversationId];
|
||||
if (data.conversation_id === state.currentConversationIdId) {
|
||||
var details = state.conversationDetails[state.currentConversationIdId];
|
||||
if (details && details.members) {
|
||||
var member = details.members.find(function (m) { return m.user_id === data.user_id; });
|
||||
if (member) {
|
||||
@ -1694,13 +1694,13 @@
|
||||
}
|
||||
}
|
||||
// Re-render to update check marks
|
||||
ChatView.renderMessages(state.messages[state.currentConversationId] || []);
|
||||
ChatView.renderMessages(state.messages[state.currentConversationIdId] || []);
|
||||
}
|
||||
},
|
||||
|
||||
handleTyping: function (data) {
|
||||
var convId = data.conversation_id;
|
||||
if (convId !== state.currentConversationId) return;
|
||||
if (convId !== state.currentConversationIdId) return;
|
||||
|
||||
var typingEl = document.getElementById('typingIndicator');
|
||||
var typingName = document.getElementById('typingName');
|
||||
@ -1732,14 +1732,14 @@
|
||||
|
||||
handleReaction: function (data) {
|
||||
var convId = data.conversation_id;
|
||||
if (convId !== state.currentConversationId) return;
|
||||
if (convId !== state.currentConversationIdId) return;
|
||||
// Reload messages to update reactions
|
||||
ChatView.loadMessages(convId);
|
||||
},
|
||||
|
||||
handleMessageEdited: function (msg) {
|
||||
var convId = msg.conversation_id;
|
||||
if (convId !== state.currentConversationId) return;
|
||||
if (convId !== state.currentConversationIdId) return;
|
||||
var msgs = state.messages[convId] || [];
|
||||
var idx = msgs.findIndex(function (m) { return m.id === msg.id; });
|
||||
if (idx !== -1) {
|
||||
@ -1750,7 +1750,7 @@
|
||||
|
||||
handleMessageDeleted: function (data) {
|
||||
var convId = data.conversation_id;
|
||||
if (convId !== state.currentConversationId) return;
|
||||
if (convId !== state.currentConversationIdId) return;
|
||||
var msgs = state.messages[convId] || [];
|
||||
var idx = msgs.findIndex(function (m) { return m.id === data.id; });
|
||||
if (idx !== -1) {
|
||||
@ -1762,8 +1762,8 @@
|
||||
|
||||
handlePresence: function (data) {
|
||||
// Update online dot in header if relevant
|
||||
if (!state.currentConversationId) return;
|
||||
var details = state.conversationDetails[state.currentConversationId];
|
||||
if (!state.currentConversationIdId) return;
|
||||
var details = state.conversationDetails[state.currentConversationIdId];
|
||||
if (!details || !details.members) return;
|
||||
|
||||
var member = details.members.find(function (m) { return m.user_id === data.user_id; });
|
||||
@ -1793,8 +1793,8 @@
|
||||
// Poll for new messages every 5 seconds when SSE is unavailable
|
||||
if (state.pollingInterval) return;
|
||||
state.pollingInterval = setInterval(function () {
|
||||
if (!state.currentConversationId) return;
|
||||
var convId = state.currentConversationId;
|
||||
if (!state.currentConversationIdId) return;
|
||||
var convId = state.currentConversationIdId;
|
||||
var msgs = state.messages[convId];
|
||||
if (!msgs || !msgs.length) return;
|
||||
|
||||
@ -1889,8 +1889,8 @@
|
||||
|
||||
startPolling: function () {
|
||||
state.presenceInterval = setInterval(function () {
|
||||
if (state.currentConversationId) {
|
||||
Presence.fetchForConversation(state.currentConversationId);
|
||||
if (state.currentConversationIdId) {
|
||||
Presence.fetchForConversation(state.currentConversationIdId);
|
||||
}
|
||||
}, 60000);
|
||||
},
|
||||
@ -2513,7 +2513,7 @@
|
||||
},
|
||||
|
||||
loadDetails: async function () {
|
||||
var convId = state.currentConversation;
|
||||
var convId = state.currentConversationId;
|
||||
if (!convId) return;
|
||||
|
||||
var details = state.conversationDetails[convId];
|
||||
@ -2528,19 +2528,22 @@
|
||||
var nameInput = document.getElementById('groupEditName');
|
||||
if (nameInput) nameInput.value = details.name || '';
|
||||
|
||||
// Owner check
|
||||
var isOwner = details.members.some(function (m) {
|
||||
return m.user_id === window.__CURRENT_USER__.id && m.role === 'owner';
|
||||
// Check caller's role
|
||||
var myMembership = details.members.find(function (m) {
|
||||
return m.user_id === window.__CURRENT_USER__.id;
|
||||
});
|
||||
var myRole = myMembership ? myMembership.role : 'member';
|
||||
var canManage = myRole === 'owner' || myRole === 'admin';
|
||||
var isOwner = myRole === 'owner';
|
||||
|
||||
// Save name — show only for owner
|
||||
// Save name — show for owner/admin
|
||||
var saveBtn = document.getElementById('saveGroupName');
|
||||
if (saveBtn) saveBtn.style.display = isOwner ? '' : 'none';
|
||||
if (nameInput) nameInput.readOnly = !isOwner;
|
||||
if (saveBtn) saveBtn.style.display = canManage ? '' : 'none';
|
||||
if (nameInput) nameInput.readOnly = !canManage;
|
||||
|
||||
// Add member section — owner only
|
||||
// Add member section — owner/admin
|
||||
var addSection = document.getElementById('groupAddMemberSection');
|
||||
if (addSection) addSection.style.display = isOwner ? '' : 'none';
|
||||
if (addSection) addSection.style.display = canManage ? '' : 'none';
|
||||
|
||||
// Members list
|
||||
var countEl = document.getElementById('groupMemberCount');
|
||||
@ -2550,6 +2553,8 @@
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = '';
|
||||
|
||||
var roleLabels = { owner: 'Właściciel', admin: 'Administrator', member: 'Członek' };
|
||||
|
||||
details.members.forEach(function (m) {
|
||||
var item = el('div', 'group-member-item');
|
||||
|
||||
@ -2562,11 +2567,9 @@
|
||||
|
||||
var info = el('div', 'group-member-info');
|
||||
var nameEl = el('div', 'group-member-name', m.name);
|
||||
var roleEl = el('span', 'group-member-role', ' \u2014 ' + (roleLabels[m.role] || m.role));
|
||||
nameEl.appendChild(roleEl);
|
||||
info.appendChild(nameEl);
|
||||
if (m.role === 'owner') {
|
||||
var roleEl = el('span', 'group-member-role', ' (właściciel)');
|
||||
nameEl.appendChild(roleEl);
|
||||
}
|
||||
if (m.company_name) {
|
||||
var companyEl = el('div', 'group-member-role', m.company_name);
|
||||
info.appendChild(companyEl);
|
||||
@ -2575,14 +2578,37 @@
|
||||
item.appendChild(avatar);
|
||||
item.appendChild(info);
|
||||
|
||||
// Actions (owner can remove others, anyone can see)
|
||||
if (isOwner && m.user_id !== window.__CURRENT_USER__.id) {
|
||||
// Actions
|
||||
var isMe = m.user_id === window.__CURRENT_USER__.id;
|
||||
if (!isMe && m.role !== 'owner') {
|
||||
var actions = el('div', 'group-member-actions');
|
||||
var removeBtn = el('button', 'btn-member-action danger', 'Usuń');
|
||||
removeBtn.addEventListener('click', function () {
|
||||
GroupManager.removeMember(convId, m.user_id, m.name);
|
||||
});
|
||||
actions.appendChild(removeBtn);
|
||||
|
||||
// Role toggle — only owner can change roles
|
||||
if (isOwner) {
|
||||
if (m.role === 'member') {
|
||||
var promoteBtn = el('button', 'btn-member-action', 'Nadaj admina');
|
||||
promoteBtn.addEventListener('click', function () {
|
||||
GroupManager.changeRole(convId, m.user_id, 'admin');
|
||||
});
|
||||
actions.appendChild(promoteBtn);
|
||||
} else if (m.role === 'admin') {
|
||||
var demoteBtn = el('button', 'btn-member-action', 'Odbierz admina');
|
||||
demoteBtn.addEventListener('click', function () {
|
||||
GroupManager.changeRole(convId, m.user_id, 'member');
|
||||
});
|
||||
actions.appendChild(demoteBtn);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove — owner or admin can remove
|
||||
if (canManage) {
|
||||
var removeBtn = el('button', 'btn-member-action danger', 'Usuń');
|
||||
removeBtn.addEventListener('click', function () {
|
||||
GroupManager.removeMember(convId, m.user_id, m.name);
|
||||
});
|
||||
actions.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
item.appendChild(actions);
|
||||
}
|
||||
|
||||
@ -2591,7 +2617,7 @@
|
||||
},
|
||||
|
||||
saveName: async function () {
|
||||
var convId = state.currentConversation;
|
||||
var convId = state.currentConversationId;
|
||||
var nameInput = document.getElementById('groupEditName');
|
||||
if (!convId || !nameInput) return;
|
||||
|
||||
@ -2632,6 +2658,17 @@
|
||||
}
|
||||
},
|
||||
|
||||
changeRole: async function (convId, userId, newRole) {
|
||||
try {
|
||||
await api('/api/conversations/' + convId + '/members/' + userId, 'PATCH', { role: newRole });
|
||||
// Refresh
|
||||
delete state.conversationDetails[convId];
|
||||
GroupManager.loadDetails();
|
||||
} catch (e) {
|
||||
alert('Nie udało się zmienić roli: ' + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
searchNewMember: async function (query) {
|
||||
var suggestions = document.getElementById('groupAddMemberSuggestions');
|
||||
if (!suggestions) return;
|
||||
@ -2640,7 +2677,7 @@
|
||||
query = (query || '').trim();
|
||||
if (query.length < 2) return;
|
||||
|
||||
var convId = state.currentConversation;
|
||||
var convId = state.currentConversationId;
|
||||
var details = state.conversationDetails[convId];
|
||||
var existingIds = details ? details.members.map(function (m) { return m.user_id; }) : [];
|
||||
|
||||
@ -2870,9 +2907,9 @@
|
||||
},
|
||||
|
||||
loadAndShow: async function () {
|
||||
if (!state.currentConversationId) return;
|
||||
if (!state.currentConversationIdId) return;
|
||||
try {
|
||||
var data = await api('/api/conversations/' + state.currentConversationId + '/pins');
|
||||
var data = await api('/api/conversations/' + state.currentConversationIdId + '/pins');
|
||||
// API returns array directly, not {pins: [...]}
|
||||
var pins = Array.isArray(data) ? data : (data.pins || []);
|
||||
|
||||
@ -2994,16 +3031,16 @@
|
||||
if (!chatMessages) return;
|
||||
|
||||
chatMessages.addEventListener('scroll', function () {
|
||||
if (chatMessages.scrollTop < 100 && state.currentConversationId) {
|
||||
var msgs = state.messages[state.currentConversationId] || [];
|
||||
var hasMore = state.hasMore[state.currentConversationId];
|
||||
if (chatMessages.scrollTop < 100 && state.currentConversationIdId) {
|
||||
var msgs = state.messages[state.currentConversationIdId] || [];
|
||||
var hasMore = state.hasMore[state.currentConversationIdId];
|
||||
if (hasMore && msgs.length > 0) {
|
||||
var oldestId = msgs[0].id;
|
||||
// Prevent multiple loads
|
||||
state.hasMore[state.currentConversationId] = false;
|
||||
state.hasMore[state.currentConversationIdId] = false;
|
||||
|
||||
var scrollHeightBefore = chatMessages.scrollHeight;
|
||||
ChatView.loadMessages(state.currentConversationId, oldestId).then(function () {
|
||||
ChatView.loadMessages(state.currentConversationIdId, oldestId).then(function () {
|
||||
// Maintain scroll position
|
||||
var scrollHeightAfter = chatMessages.scrollHeight;
|
||||
chatMessages.scrollTop = scrollHeightAfter - scrollHeightBefore;
|
||||
@ -3023,7 +3060,7 @@
|
||||
backBtn.addEventListener('click', function () {
|
||||
var container = document.getElementById('conversationsApp');
|
||||
if (container) container.classList.remove('show-chat');
|
||||
state.currentConversationId = null;
|
||||
state.currentConversationIdId = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -326,7 +326,7 @@ window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
|
||||
// Load conversations.js after data is set
|
||||
(function() {
|
||||
var s = document.createElement('script');
|
||||
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=20';
|
||||
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=21';
|
||||
document.body.appendChild(s);
|
||||
})();
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user