Merge pull request #101 from jolavillette/ImproveWebui

improve the webui and make it actually usable on a telephone
This commit is contained in:
csoler 2026-04-01 22:48:39 +02:00 committed by GitHub
commit 22b2939858
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 2535 additions and 1321 deletions

View File

@ -8,35 +8,74 @@ function loadLobbyDetails(id, apply) {
rs.rsJsonApiRequest(
'/rsChats/getChatLobbyInfo',
{
id,
id: { xstr64: id },
},
(detail) => {
if (detail.retval) {
(detail, success) => {
if (success && detail.retval) {
detail.info.chatType = 3; // LOBBY
apply(detail.info);
} else {
apply(null);
}
},
true,
{},
undefined,
// Custom serializer NOTE:
// Since id represents 64-bit int(see deserializer note below)
// Instead of using JSON.stringify, this function directly
// creates a json string manually.
() => '{"id":' + id + '}'
true
);
}
function loadDistantChatDetails(pid, apply) {
// pid is DistantChatPeerId (uint32)
rs.rsJsonApiRequest(
'/rsChats/getDistantChatStatus',
{
pid: pid,
},
(detail, success) => {
if (success && detail.retval) {
// Map to lobby-like structure for UI compatibility
const info = detail.info;
info.chatType = 2; // DISTANT (matches TYPE_PRIVATE_DISTANT in rschats.h)
info.lobby_name = rs.userList.username(info.to_id) || 'Distant Chat ' + pid;
info.lobby_topic = 'Private Encrypted Chat';
info.gxs_id = info.own_id;
info.lobby_id = pid; // Distant IDs are 128-bit hex strings, NO xstr64 wrapper
apply(info);
} else {
apply(null);
}
},
true
);
}
function sortLobbies(lobbies) {
if (lobbies !== undefined) {
if (lobbies !== undefined && lobbies !== null) {
const list = [...lobbies];
list.sort((a, b) => a.lobby_name.localeCompare(b.lobby_name));
return list;
}
// return lobbies; // fallback on reload page in browser, keep undefiend
return []; // return empty array instead of undefined
}
// ***************************** models ***********************************
const MobileState = {
showLobbies: false,
showUsers: false,
toggleLobbies() {
this.showLobbies = !this.showLobbies;
this.showUsers = false;
},
toggleUsers() {
this.showUsers = !this.showUsers;
this.showLobbies = false;
},
closeAll() {
this.showLobbies = false;
this.showUsers = false;
},
};
const ChatRoomsModel = {
allRooms: [],
knownSubscrIds: [], // to exclude subscribed from public rooms (subscribedRooms filled to late)
@ -47,40 +86,73 @@ const ChatRoomsModel = {
rs.rsJsonApiRequest(
'/rsChats/getListOfNearbyChatLobbies',
{},
(data) => (ChatRoomsModel.allRooms = sortLobbies(data.public_lobbies))
(data) => {
if (data && data.public_lobbies) {
// Deduplicate by ID to avoid double display if backend returns redundant info
const seen = new Set();
const uniqueLobbies = data.public_lobbies.filter((lobby) => {
const id = rs.idToHex(lobby.lobby_id);
if (seen.has(id)) return false;
seen.add(id);
return true;
});
ChatRoomsModel.allRooms = sortLobbies(uniqueLobbies);
} else {
// No public lobbies
ChatRoomsModel.allRooms = [];
}
}
);
},
loadSubscribedRooms(after = null) {
// ChatRoomsModel.subscribedRooms = {};
rs.rsJsonApiRequest(
'/rsChats/getChatLobbyList',
{},
// JS uses double precision numbers of 64 bit. It is equivalent
// to 53 bits of precision. All large precision ints will
// get truncated to an approximation.
// This API uses Cpp-style 64 bits for `id`.
// So we use the string-value 'xstr64' instead
(data) => {
const ids = data.cl_list.map((lid) => lid.xstr64);
ChatRoomsModel.knownSubscrIds = ids;
const rooms = {};
ids.map((id) =>
loadLobbyDetails(id, (info) => {
rooms[id] = info;
if (Object.keys(rooms).length === ids.length) {
// apply rooms to subscribedRooms only after reading all room-details, so sorting all or nothing
ChatRoomsModel.subscribedRooms = rooms;
if (data && data.cl_list) {
// Robust deduplication of IDs
const ids = [...new Set(data.cl_list.map((lid) => rs.idToHex(lid)))];
ChatRoomsModel.knownSubscrIds = ids;
// Remove stale entries that are no longer in the subscribed list
Object.keys(ChatRoomsModel.subscribedRooms).forEach((id) => {
if (!ids.includes(id)) {
delete ChatRoomsModel.subscribedRooms[id];
}
})
);
if (after != null) {
after();
});
if (ids.length === 0) {
ChatRoomsModel.loadPublicRooms();
if (after != null) after();
m.redraw();
return;
}
let count = 0;
ids.forEach((id) =>
loadLobbyDetails(id, (info) => {
if (info) {
ChatRoomsModel.subscribedRooms[id] = info;
}
count++;
if (count === ids.length) {
ChatRoomsModel.loadPublicRooms(); // Load public rooms after we know all subscribed IDs
if (after != null) {
after();
}
m.redraw();
}
})
);
} else {
// No subscribed lobbies
ChatRoomsModel.loadPublicRooms();
}
}
);
},
subscribed(info) {
return this.knownSubscrIds.includes(info.lobby_id.xstr64);
return this.knownSubscrIds.includes(rs.idToHex(info.lobby_id));
},
};
@ -90,28 +162,42 @@ const ChatRoomsModel = {
* msg: Message to Display
*/
const Message = () => {
let msg = null; // message to display
let text = ''; // extracted text to display
let datetime = ''; // date time to display
let username = ''; // username to display (later may be linked)
return {
oninit: (vnode) => {
console.info('chat Message', vnode);
msg = vnode.attrs;
datetime = new Date(msg.sendTime * 1000).toLocaleTimeString();
username = rs.userList.username(msg.lobby_peer_gxs_id);
text = msg.msg
view: (vnode) => {
const msg = vnode.attrs;
const datetime = new Date(msg.sendTime * 1000).toLocaleTimeString();
// Handle both HistoryMsg (peerId) and ChatMessage (lobby_peer_gxs_id)
const rawGxsId = msg.lobby_peer_gxs_id || msg.peerId;
let gxsId = rs.idToHex(rawGxsId);
// Fallback for 1-to-1 chats where sender ID might be missing (zeros)
const isZero = (id) => !id || id === '00000000000000000000000000000000';
if (isZero(gxsId)) {
const lobby = ChatLobbyModel.currentLobby;
// Types 1 (Private), 2 (Distant) are "private" conversations here
if (lobby && (lobby.chatType === 1 || lobby.chatType === 2)) {
gxsId = msg.incoming ? rs.idToHex(lobby.to_id || lobby.peer_id || lobby.distant_chat_id) : rs.idToHex(lobby.own_id || lobby.gxs_id);
}
}
let username = rs.userList.username(gxsId) || msg.peerName || '???';
// If we only have the hex ID, try to fallback to the peerName from the message
if (username === gxsId && msg.peerName) {
username = msg.peerName;
}
if (username === gxsId && gxsId && gxsId.length > 12) {
username = gxsId.substring(0, 8) + '...';
}
const text = (msg.msg || msg.message || '')
.replaceAll('<br/>', '\n')
.replace(new RegExp('<style[^<]*</style>|<[^>]*>', 'gm'), '');
console.info('chat Text', text);
},
view: () =>
m(
return m(
'.message',
m('span.datetime', datetime),
m('span.username', username),
m('span.messagetext', text)
),
);
},
};
};
@ -123,86 +209,228 @@ const ChatLobbyModel = {
isSubscribed: false,
messages: [],
users: [],
setupAction: (lobbyId, nick) => {},
messageKeys: new Set(),
lastLobbyId: null,
// Helper to generate a unique key for deduplication
getMessageKey(msg) {
if (msg.msgId && msg.msgId !== 0) return 'id_' + msg.msgId;
// Fallback for live messages or history without IDs
const text = msg.msg || msg.message || '';
return 't_' + msg.sendTime + '_' + text.substring(0, 32);
},
addMessages(newMsgs, scroll = false) {
let added = false;
newMsgs.forEach((msg) => {
const key = this.getMessageKey(msg);
if (!this.messageKeys.has(key)) {
// Near-duplicate check for messages without IDs (live events vs optimistic echo)
const text = msg.msg || msg.message || '';
const isNearDuplicate = this.messages.some((existingMsg) => {
const eAttrs = existingMsg.attrs;
const eText = eAttrs.msg || eAttrs.message || '';
return (
eText === text &&
Math.abs(eAttrs.sendTime - msg.sendTime) < 5 // 5 seconds window
);
});
if (!isNearDuplicate) {
this.messageKeys.add(key);
this.messages.push(m(Message, msg));
added = true;
}
}
});
if (added) {
this.messages.sort((a, b) => a.attrs.sendTime - b.attrs.sendTime);
m.redraw();
if (scroll) {
setTimeout(() => {
const element = document.querySelector('.messages');
if (element) {
element.scrollTop = element.scrollHeight;
}
}, 100);
}
}
},
loadHistory(id, type) {
const chatPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: type,
peer_id: '00000000000000000000000000000000',
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
};
if (type === 3) chatPeerId.lobby_id.xstr64 = id;
else if (type === 2) chatPeerId.distant_chat_id = id;
else if (type === 1) chatPeerId.peer_id = id;
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: chatPeerId,
loadCount: 20,
},
(data, success) => {
if (success && data.msgs) {
this.addMessages(data.msgs);
}
}
);
},
setupAction: (lobbyId, nick) => { },
setIdentity(lobbyId, nick) {
rs.rsJsonApiRequest(
'/rsChats/setIdentityForChatLobby',
{},
() => m.route.set('/chat/:lobby_id', { lobbyId }),
true,
{},
JSON.parse,
() => '{"lobby_id":' + lobbyId + ',"nick":"' + nick + '"}'
{
lobby_id: { xstr64: lobbyId },
nick: nick,
},
() => m.route.set('/chat/:lobby', { lobby: lobbyId }),
true
);
},
enterPublicLobby(lobbyId, nick) {
console.info('joinVisibleChatLobby', nick, '@', lobbyId);
// Set lobby nickname
rs.rsJsonApiRequest(
'/rsChats/joinVisibleChatLobby',
{},
{
lobby_id: { xstr64: lobbyId },
own_id: nick,
},
() => {
loadLobbyDetails(lobbyId, (info) => {
ChatRoomsModel.subscribedRooms[lobbyId] = info;
ChatRoomsModel.loadSubscribedRooms(() => {
m.route.set('/chat/:lobby', { lobby: info.lobby_id.xstr64 });
m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) });
});
});
},
true,
{},
JSON.parse,
() => '{"lobby_id":' + lobbyId + ',"own_id":"' + nick + '"}'
true
);
},
unsubscribeChatLobby(lobbyId, follow) {
console.info('unsubscribe lobby', lobbyId);
// Unsubscribe
rs.rsJsonApiRequest(
'/rsChats/unsubscribeChatLobby',
{},
() => ChatRoomsModel.loadSubscribedRooms(follow),
true,
{},
JSON.parse,
() => '{"lobby_id":' + lobbyId + '}'
{
lobby_id: { xstr64: lobbyId },
},
(data, success) => {
if (success) {
ChatRoomsModel.loadSubscribedRooms(follow);
}
},
true
);
},
chatId(action) {
return { type: 3, lobby_id: { xstr64: m.route.param('lobby') } };
chatId() {
const type = (this.currentLobby && this.currentLobby.chatType) || 3;
const id = this.lastLobbyId || m.route.param('lobby');
const cid = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: type,
peer_id: '00000000000000000000000000000000',
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
};
if (type === 3) cid.lobby_id.xstr64 = id;
else if (type === 2) cid.distant_chat_id = id;
else if (type === 1) cid.peer_id = id;
return cid;
},
loadLobby(currentlobbyid) {
loadLobbyDetails(currentlobbyid, (detail) => {
this.lastLobbyId = currentlobbyid;
const finishLoad = (detail) => {
this.setupAction = this.setIdentity;
this.currentLobby = detail;
this.isSubscribed = true;
this.lobby_user = rs.userList.username(detail.gxs_id) || '???';
const lobbyid = currentlobbyid;
// apply existing messages to current lobby view
rs.events[15].chatMessages(
this.chatId(),
rs.events[15],
(l) => (this.messages = l.map((msg) => m(Message, msg)))
);
// register for chatEvents for future messages
// Reset local state for this lobby
this.messages = [];
this.messageKeys.clear();
// Load history first
this.loadHistory(currentlobbyid, detail.chatType);
// Apply existing messages from live cache
const cid = this.chatId();
rs.events[15].chatMessages(cid, rs.events[15], (l) => {
this.addMessages(l);
});
// Register for chatEvents for future messages
rs.events[15].notify = (chatMessage) => {
if (chatMessage.chat_id.type === 3 && chatMessage.chat_id.lobby_id.xstr64 === lobbyid) {
this.messages.push(m(Message, chatMessage));
m.redraw();
// DEBUG: Log incoming message structure
console.log('[RS-DEBUG] Incoming Chat Message:', JSON.stringify(chatMessage, null, 2));
const msgCid = chatMessage.chat_id;
let msgId;
if (msgCid.type === 3) {
msgId = rs.idToHex(msgCid.lobby_id);
} else if (msgCid.type === 2) {
// For Distant Chat, the ID is the distant_chat_id
msgId = rs.idToHex(msgCid.distant_chat_id);
} else if (msgCid.type === 1) {
// For Private Chat, the ID is the peer_id
msgId = rs.idToHex(msgCid.peer_id);
} else {
// Fallback
msgId = rs.idToHex(msgCid);
}
console.log('[RS-DEBUG] Resolved Msg ID:', msgId, 'Current Lobby ID:', currentlobbyid, 'Match:', msgId === currentlobbyid);
if (msgId === currentlobbyid) {
this.addMessages([chatMessage]);
}
};
// lookup for chat-user names (only snapshot, we don't get notified about changes of participants)
const names = detail.gxs_ids.reduce((a, u) => a.concat(rs.userList.username(u.key)), []);
names.sort((a, b) => a.localeCompare(b));
this.users = [];
names.forEach((name) => (this.users = this.users.concat([m('.user', name)])));
return this.users;
// Lookup for chat-user names (Only for lobbies for now)
// Lookup for chat-user names
if (detail.gxs_ids) {
let names = [];
if (Array.isArray(detail.gxs_ids)) {
names = detail.gxs_ids.reduce((a, u) => a.concat(rs.userList.username(u.key)), []);
} else if (typeof detail.gxs_ids === 'object') {
names = Object.keys(detail.gxs_ids).map(key => rs.userList.username(key));
}
names.sort((a, b) => a.localeCompare(b));
this.users = [];
names.forEach((name) => (this.users = this.users.concat([m('.user', name)])));
} else {
this.users = [m('.user', detail.lobby_name)];
}
m.redraw();
};
loadLobbyDetails(currentlobbyid, (detail) => {
if (detail) {
finishLoad(detail);
} else {
// Fallback to Distant Chat
loadDistantChatDetails(currentlobbyid, (dDetail) => {
if (dDetail) {
finishLoad(dDetail);
}
});
}
});
},
loadPublicLobby(currentlobbyid) {
console.info('loadPublicLobby ChatRoomsModel:', ChatRoomsModel);
this.setupAction = this.enterPublicLobby;
this.isSubscribed = false;
ChatRoomsModel.allRooms.forEach((it) => {
if (it.lobby_id.xstr64 === currentlobbyid) {
if (rs.idToHex(it.lobby_id) === currentlobbyid) {
this.currentLobby = it;
this.lobby_user = '???';
this.lobbyid = currentlobbyid;
@ -211,51 +439,47 @@ const ChatLobbyModel = {
this.users = [];
},
sendMessage(msg, onsuccess) {
const cid = this.chatId();
// Optimistic echo for immediate feedback
const echoMsg = {
chat_id: cid,
msg: msg,
sendTime: Math.floor(Date.now() / 1000),
lobby_peer_gxs_id: this.currentLobby.gxs_id,
};
this.addMessages([echoMsg], true);
rs.rsJsonApiRequest(
'/rsChats/sendChat',
{},
() => {
// adding own message to log
rs.events[15].handler(
{
mChatMessage: {
chat_id: this.chatId(),
msg,
sendTime: new Date().getTime() / 1000,
lobby_peer_gxs_id: this.currentLobby.gxs_id,
},
},
rs.events[15]
);
onsuccess();
{
id: cid,
msg: msg,
},
true,
{},
undefined,
() =>
'{"id":{"type": 3,"lobby_id":' +
m.route.param('lobby') +
'}, "msg":' +
JSON.stringify(msg) +
'}'
(data, success) => {
if (success) {
onsuccess();
} else {
console.error('[RS] Failed to send chat message');
onsuccess(); // Clear the input even on failure to avoid stuck 'sending...' state
}
}
);
},
selected(info, selName, defaultName) {
const currid = (ChatLobbyModel.currentLobby.lobby_id || { xstr64: m.route.param('lobby') })
.xstr64;
return (info.lobby_id.xstr64 === currid ? selName : '') + defaultName;
const currid = rs.idToHex(ChatLobbyModel.currentLobby.lobby_id || { xstr64: m.route.param('lobby') });
return (rs.idToHex(info.lobby_id) === currid ? selName : '') + defaultName;
},
switchToEvent(info) {
return () => {
ChatLobbyModel.currentLobby = info;
m.route.set('/chat/:lobby', { lobby: info.lobby_id.xstr64 });
ChatLobbyModel.loadLobby(info.lobby_id.xstr64); // update
m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) });
ChatLobbyModel.loadLobby(rs.idToHex(info.lobby_id)); // update
};
},
setupEvent(info) {
return () => {
m.route.set('/chat/:lobby/setup', { lobby: info.lobby_id.xstr64 });
ChatLobbyModel.loadPublicLobby(info.lobby_id.xstr64); // update
m.route.set('/chat/:lobby/setup', { lobby: rs.idToHex(info.lobby_id) });
ChatLobbyModel.loadPublicLobby(rs.idToHex(info.lobby_id)); // update
};
},
};
@ -263,23 +487,13 @@ const ChatLobbyModel = {
// ************************* views ****************************
const Lobby = () => {
let info = {};
let tagname = '';
let onclick = (e) => {};
let lobbytagname = '';
return {
oninit: (v) => {
info = v.attrs.info;
tagname = v.attrs.tagname;
onclick = v.attrs.onclick || ((e) => {});
lobbytagname = v.attrs.lobbytagname || 'mainname';
},
view: (v) => {
view: (vnode) => {
const { info, tagname, onclick, lobbytagname = 'mainname' } = vnode.attrs;
return m(
ChatLobbyModel.selected(info, '.selected-lobby', tagname),
{
key: info.lobby_id.xstr64,
key: rs.idToHex(info.lobby_id),
onclick,
},
[
@ -343,7 +557,7 @@ const PublicLeftLobbies = {
return [
m('h5.lefttitle', 'public:'),
m(LobbyList, {
rooms: Object.values(ChatRoomsModel.allRooms).filter(
rooms: Object.values(ChatRoomsModel.allRooms || {}).filter(
(info) => !ChatRoomsModel.subscribed(info)
),
tagname: '.leftlobby.public',
@ -354,89 +568,158 @@ const PublicLeftLobbies = {
},
};
const PublicLobbies = () => {
return m('.widget', [
m('.widget__heading', m('h3', 'Public chat rooms')),
m('.widget__body', [
m(LobbyList, {
rooms: ChatRoomsModel.allRooms.filter((info) => !ChatRoomsModel.subscribed(info)),
tagname: '.lobby.public',
onclick: ChatLobbyModel.setupEvent,
}),
]),
]);
const PublicLobbies = {
view() {
return m('.widget', [
m('.widget__heading', m('h3', 'Public chat rooms')),
m('.widget__body', [
m(LobbyList, {
rooms: (ChatRoomsModel.allRooms || []).filter((info) => !ChatRoomsModel.subscribed(info)),
tagname: '.lobby.public',
onclick: ChatLobbyModel.setupEvent,
}),
]),
]);
},
};
const LobbyName = () => {
return m(
'h3.lobbyName',
m('.mobile-menu-icons', [
m('i.fas.fa-bars', { onclick: () => MobileState.toggleLobbies() }),
]),
ChatLobbyModel.isSubscribed
? [m('span.chatusername', ChatLobbyModel.lobby_user), m('span.chatatchar', '@')]
: [],
ChatLobbyModel.currentLobby.chatType === 2
? m('i.fas.fa-circle', {
style: {
color:
ChatLobbyModel.currentLobby.status === 2
? '#2ecc71' // Green (Can Talk)
: ChatLobbyModel.currentLobby.status === 1
? '#f39c12' // Orange (Tunnel Down)
: ChatLobbyModel.currentLobby.status === 3
? '#e74c3c' // Red (Remotely Closed)
: '#95a5a6', // Grey (Unknown)
fontSize: '0.6em',
marginRight: '10px',
verticalAlign: 'middle',
},
title:
ChatLobbyModel.currentLobby.status === 2
? 'Tunnel Active (Can Talk)'
: ChatLobbyModel.currentLobby.status === 1
? 'Tunnel Down (Negotiating...)'
: ChatLobbyModel.currentLobby.status === 3
? 'Remotely Closed'
: 'Status Unknown',
})
: [],
m('span.chatlobbyname', ChatLobbyModel.currentLobby.lobby_name),
m.route.param('subaction') !== 'setup'
m('.mobile-menu-icons', [
m('i.fas.fa-users', { onclick: () => MobileState.toggleUsers() }),
]),
m.route.param('subaction') !== 'setup' && ChatLobbyModel.currentLobby.chatType === 3
? [
m('i.fas.fa-cog.setupicon', {
title: 'configure lobby',
onclick: () =>
m.route.set(
'/chat/:lobby/:subaction',
{
lobby: m.route.param('lobby'),
subaction: 'setup',
},
{ replace: true }
),
}),
]
m('i.fas.fa-cog.setupicon', {
title: 'configure lobby',
onclick: () =>
m.route.set(
'/chat/:lobby/:subaction',
{
lobby: m.route.param('lobby'),
subaction: 'setup',
},
{ replace: true }
),
}),
]
: [],
ChatLobbyModel.isSubscribed
? [
m('i.fas.fa-sign-out-alt.leaveicon', {
title: 'leaving lobby',
onclick: () =>
ChatLobbyModel.unsubscribeChatLobby(m.route.param('lobby'), () => {
m.route.set('/chat', null, { replace: true });
}),
}),
]
m('i.fas.fa-sign-out-alt.leaveicon', {
title: 'leaving lobby',
onclick: () =>
ChatLobbyModel.unsubscribeChatLobby(m.route.param('lobby'), () => {
m.route.set('/chat', null, { replace: true });
}),
}),
]
: []
);
};
// ***************************** Page Layouts ******************************
const Layout = () => {
return {
view: () => m('.node-panel', [m(SubscribedLobbies), PublicLobbies()]),
};
const Layout = {
view: () => m('.node-panel.chat-panel.chat-hub', [m(SubscribedLobbies), m(PublicLobbies)]),
};
const LayoutSingle = () => {
const onResize = () => {
const element = document.querySelector('.messages');
if (element) element.scrollTop = element.scrollHeight;
};
return {
oninit: () => ChatLobbyModel.loadLobby(m.route.param('lobby')),
view: (vnode) =>
m('.node-panel', [
LobbyName(),
m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)),
m('.messages', ChatLobbyModel.messages),
m('.rightbar', ChatLobbyModel.users),
m(
'.chatMessage',
{},
m('textarea.chatMsg', {
placeholder: 'enter new message and press return to send',
onkeydown: (e) => {
if (e.code === 'Enter') {
const msg = e.target.value;
e.target.value = ' sending ... ';
ChatLobbyModel.sendMessage(msg, () => (e.target.value = ''));
return false;
}
},
})
),
]),
oninit: () => {
ChatLobbyModel.loadLobby(m.route.param('lobby'));
window.addEventListener('resize', onResize);
},
onremove: () => window.removeEventListener('resize', onResize),
view: (vnode) => {
const chatType = ChatLobbyModel.currentLobby.chatType;
const isPrivate = chatType === 1 || chatType === 2;
return m(
'.node-panel.chat-panel.chat-room',
{
class:
(MobileState.showLobbies ? 'show-lobbies ' : '') +
(MobileState.showUsers ? 'show-users ' : '') +
(isPrivate ? 'no-lobbies' : ''),
},
[
m('.chat-overlay', { onclick: () => MobileState.closeAll() }),
LobbyName(),
!isPrivate && m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)),
m('.messages', { onclick: () => MobileState.closeAll() }, ChatLobbyModel.messages),
m('.rightbar', ChatLobbyModel.users),
m(
'.chatMessage',
{},
[
m('textarea.chatMsg', {
placeholder: 'Type a message...',
enterkeyhint: 'send',
onkeydown: (e) => {
if ((e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey) {
const msg = e.target.value;
if (msg.trim() === '') return false;
e.target.value = ' sending ... ';
ChatLobbyModel.sendMessage(msg, () => (e.target.value = ''));
return false;
}
},
}),
m(
'button.chat-send-btn',
{
onclick: (e) => {
const textarea = e.target.closest('.chatMessage').querySelector('textarea');
const msg = textarea.value;
if (msg.trim() === '') return;
textarea.value = ' sending ... ';
ChatLobbyModel.sendMessage(msg, () => (textarea.value = ''));
},
},
m('i.fas.fa-paper-plane')
),
]
),
]
);
},
};
};
@ -445,40 +728,49 @@ const LayoutSetup = () => {
return {
oninit: () => peopleUtil.ownIds((data) => (ownIds = data)),
view: (vnode) =>
m('.node-panel', [
LobbyName(),
m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)),
m('.setup', [
m('h5.selectidentity', 'Select identity to use'),
ownIds.map((nick) =>
m(
'.identity' +
m(
'.node-panel.chat-panel.chat-room.chat-setup',
{
class:
(MobileState.showLobbies ? 'show-lobbies ' : '') +
(MobileState.showUsers ? 'show-users' : ''),
},
[
m('.chat-overlay', { onclick: () => MobileState.closeAll() }),
LobbyName(),
m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)),
m('.setup', [
m('h5.selectidentity', 'Select identity to use'),
ownIds.map((nick) =>
m(
'.identity' +
(ChatLobbyModel.currentLobby.gxs_id === nick ? '.selectedidentity' : ''),
{
onclick: () => ChatLobbyModel.setupAction(m.route.param('lobby'), nick),
},
rs.userList.username(nick)
)
),
]),
]),
{
onclick: () => ChatLobbyModel.setupAction(m.route.param('lobby'), nick),
},
rs.userList.username(nick)
)
),
]),
]
),
};
};
/*
/rsChats/initiateDistantChatConnexion
* @param[in] to_pid RsGxsId to start the connection
* @param[in] from_pid owned RsGxsId who start the connection
* @param[out] pid distant chat id
* @param[out] error_code if the connection can't be stablished
* @param[in] notify notify remote that the connection is stablished
* @param[in] to_pid RsGxsId to start the connection
* @param[in] from_pid owned RsGxsId who start the connection
* @param[out] pid distant chat id
* @param[out] error_code if the connection can't be stablished
* @param[in] notify notify remote that the connection is stablished
*/
const LayoutCreateDistant = () => {
let ownIds = [];
return {
oninit: () => peopleUtil.ownIds((data) => (ownIds = data)),
view: (vnode) =>
m('.node-panel', [
m('.node-panel.chat-panel.chat-room', [
m('.createDistantChat', [
'choose identitiy to chat with ',
rs.userList.username(m.route.param('lobby')),
@ -494,9 +786,8 @@ const LayoutCreateDistant = () => {
from_pid: id,
notify: true,
},
(result) => {
console.info('initiateDistantChatConnexion', result);
m.route.set('/chat/:lobbyid', { lobbyid: result.pid });
(res) => {
m.route.set('/chat/:lobby', { lobby: rs.idToHex(res.pid) });
}
),
},
@ -511,7 +802,6 @@ const LayoutCreateDistant = () => {
module.exports = {
oninit: () => {
ChatRoomsModel.loadSubscribedRooms();
ChatRoomsModel.loadPublicRooms();
},
view: (vnode) => {
if (m.route.param('lobby') === undefined) {

View File

@ -149,24 +149,24 @@ const Component = () => {
Downloads.resetSearch();
},
view: () => [
m('.widget__body-heading', [
m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`),
m('.action', [
m('.widget__body-heading', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } }, [
m('.action', { style: { marginBottom: '10px' } }, [
m('button', { onclick: () => widget.popupMessage(m(NewFileDialog)) }, 'Add new file'),
m('button', { onclick: clearFileCompleted }, 'Clear completed'),
]),
m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`),
]),
m('.widget__body-content', [
Downloads.statusMap &&
Object.keys(Downloads.statusMap).map((hash) =>
m(util.File, {
info: Downloads.statusMap[hash],
strategy: Downloads.strategies[hash],
direction: 'down',
transferred: Downloads.statusMap[hash].transfered.xint64,
chunksInfo: Downloads.chunksMap[hash],
})
),
Object.keys(Downloads.statusMap).map((hash) =>
m(util.File, {
info: Downloads.statusMap[hash],
strategy: Downloads.strategies[hash],
direction: 'down',
transferred: Downloads.statusMap[hash].transfered.xint64,
chunksInfo: Downloads.chunksMap[hash],
})
),
]),
],
};

View File

@ -36,11 +36,10 @@ function loadSharedDirectories() {
// Update Shared Directories when there is a corresponding event
rs.events[rs.RsEventsType.SHARED_DIRECTORIES] = {
handler: (event) => {
console.log('Shared Directories Event: ', event);
switch(event.mEventCode) {
case futil.RsSharedDirectoriesEventCode.SHARED_DIRS_LIST_CHANGED:
loadSharedDirectories();
break;
switch (event.mEventCode) {
case futil.RsSharedDirectoriesEventCode.SHARED_DIRS_LIST_CHANGED:
loadSharedDirectories();
break;
}
},
};
@ -156,85 +155,85 @@ const ShareDirTable = () => {
m(
'tbody.share-manager__table_body',
sharedDirArr.length &&
sharedDirArr.map((sharedDirItem, index) => {
const {
filename,
virtualname,
shareflags,
parent_groups: parentGroups,
} = sharedDirItem;
const sharedFlags = futil.calcIndividualFlags(shareflags);
return m('tr', [
m(
'td',
m('input[type=text]', {
value: filename,
disabled: isEditDisabled,
oninput: (e) => {
sharedDirArr[index].filename = e.target.value;
},
})
),
m(
'td',
m('input[type=text]', {
value: virtualname,
disabled: isEditDisabled,
oninput: (e) => {
sharedDirArr[index].virtualname = e.target.value;
},
})
),
m(
'td.share-flags',
Object.keys(sharedFlags).map((flag) => {
return [
m(`input.share-flags-check[type=checkbox][id=${flag}]`, {
checked: sharedFlags[flag],
disabled: isEditDisabled,
}),
m(
`label.share-flags-label[for=${flag}]`,
{
onclick: () => {
if (isEditDisabled) return;
sharedFlags[flag] = !sharedFlags[flag];
sharedDirArr[index].shareflags = futil.calcShareFlagsValue(sharedFlags);
},
style: isEditDisabled && { color: '#7D7D7D' },
sharedDirArr.map((sharedDirItem, index) => {
const {
filename,
virtualname,
shareflags,
parent_groups: parentGroups,
} = sharedDirItem;
const sharedFlags = futil.calcIndividualFlags(shareflags);
return m('tr', [
m(
'td',
m('input[type=text]', {
value: filename,
disabled: isEditDisabled,
oninput: (e) => {
sharedDirArr[index].filename = e.target.value;
},
})
),
m(
'td',
m('input[type=text]', {
value: virtualname,
disabled: isEditDisabled,
oninput: (e) => {
sharedDirArr[index].virtualname = e.target.value;
},
})
),
m(
'td.share-flags',
Object.keys(sharedFlags).map((flag) => {
return [
m(`input.share-flags-check[type=checkbox][id=${flag}]`, {
checked: sharedFlags[flag],
disabled: isEditDisabled,
}),
m(
`label.share-flags-label[for=${flag}]`,
{
onclick: () => {
if (isEditDisabled) return;
sharedFlags[flag] = !sharedFlags[flag];
sharedDirArr[index].shareflags = futil.calcShareFlagsValue(sharedFlags);
},
m(
// check the flag type then if its value is true then only render the icon
flag === 'isAnonymousSearch'
? sharedFlags[flag]
? 'i.fas.fa-search'
: 'span'
: flag === 'isAnonymousDownload'
style: isEditDisabled && { color: '#7D7D7D' },
},
m(
// check the flag type then if its value is true then only render the icon
flag === 'isAnonymousSearch'
? sharedFlags[flag]
? 'i.fas.fa-search'
: 'span'
: flag === 'isAnonymousDownload'
? sharedFlags[flag]
? 'i.fas.fa-download'
: 'span'
: sharedFlags[flag]
? 'i.fas.fa-eye'
: 'span'
)
),
];
})
),
m(
'td',
{
// since this is not an input element, manually change color
style: { color: isEditDisabled ? '#6D6D6D' : 'black' },
onclick: () =>
!isEditDisabled && widget.popupMessage(m(ManageVisibility, { parentGroups })),
},
parentGroups.length === 0
? 'All Friend nodes'
: parentGroups.map((groupFlag) => futil.RsNodeGroupId[groupFlag]).join(', ')
),
]);
})
? 'i.fas.fa-eye'
: 'span'
)
),
];
})
),
m(
'td',
{
// since this is not an input element, manually change color
style: { color: isEditDisabled ? '#6D6D6D' : 'black' },
onclick: () =>
!isEditDisabled && widget.popupMessage(m(ManageVisibility, { parentGroups })),
},
parentGroups.length === 0
? 'All Friend nodes'
: parentGroups.map((groupFlag) => futil.RsNodeGroupId[groupFlag]).join(', ')
),
]);
})
),
]);
},

View File

@ -8,14 +8,19 @@ const fileProxyObj = futil.createProxy({}, () => {
rs.events[rs.RsEventsType.FILE_TRANSFER] = {
handler: (event) => {
console.log('search results : ', event);
// if request item doesn't already exists in Object then create new item
if (!Object.prototype.hasOwnProperty.call(fileProxyObj, event.mRequestId)) {
fileProxyObj[event.mRequestId] = [];
}
fileProxyObj[event.mRequestId].push(...event.mResults);
event.mResults.forEach((newRes) => {
const isAlt = fileProxyObj[event.mRequestId].some(
(oldRes) => oldRes.fHash === newRes.fHash && oldRes.fName === newRes.fName
);
if (!isAlt) {
fileProxyObj[event.mRequestId].push(newRes);
}
});
},
};

View File

@ -15,7 +15,7 @@ function handleSubmit() {
reqObj['_' + res.body.retval] = matchString;
currentItem = '_' + res.body.retval;
})
.catch((error) => console.log(error));
.catch((error) => { });
}
const SearchBar = () => {
@ -31,6 +31,33 @@ const SearchBar = () => {
};
};
const getFileIcon = (fileName) => {
const ext = fileName.split('.').pop().toLowerCase();
switch (ext) {
case 'pdf': return 'i.fas.fa-file-pdf';
case 'zip':
case 'rar':
case 'tar':
case 'gz':
case '7z': return 'i.fas.fa-file-archive';
case 'jpg':
case 'jpeg':
case 'png':
case 'gif': return 'i.fas.fa-file-image';
case 'mp4':
case 'mkv':
case 'avi':
case 'mov': return 'i.fas.fa-file-video';
case 'mp3':
case 'wav':
case 'flac': return 'i.fas.fa-file-audio';
case 'txt':
case 'doc':
case 'docx': return 'i.fas.fa-file-alt';
default: return 'i.fas.fa-file';
}
};
const Layout = () => {
let active = 0;
function handleFileDownload(item) {
@ -54,7 +81,7 @@ const Layout = () => {
);
})
.catch((error) => {
console.log('error in sending download request: ', error);
// console.log('error in sending download request: ', error);
});
}
return {
@ -63,58 +90,76 @@ const Layout = () => {
m('.widget__body', [
m('div.file-search-container', [
m('div.file-search-container__keywords', [
m('h5.bold', 'Keywords'),
Object.keys(reqObj).length !== 0 &&
m('.keywords-header', [
m('h5.bold', 'Keywords'),
m(
'div.keywords-container',
Object.keys(reqObj)
.reverse()
.map((item, index) => {
return m(
m.route.Link,
{
class: active === index ? 'selected' : '',
onclick: () => {
active = index;
currentItem = item;
},
href: `/files/search/${item}`,
},
reqObj[item]
);
})
'button.red.clear-btn',
{
onclick: () => {
Object.keys(reqObj).forEach((key) => delete reqObj[key]);
Object.keys(fproxy.fileProxyObj).forEach((key) => delete fproxy.fileProxyObj[key]);
currentItem = 0;
active = 0;
},
},
'Clear'
),
]),
Object.keys(reqObj).length !== 0 &&
m(
'div.keywords-container',
Object.keys(reqObj)
.reverse()
.map((item, index) => {
return m(
m.route.Link,
{
class: active === index ? 'selected' : '',
onclick: () => {
active = index;
currentItem = item;
},
href: `/files/search/${item}`,
},
reqObj[item]
);
})
),
]),
m('div.file-search-container__results', [
Object.keys(fproxy.fileProxyObj).length === 0
Object.keys(fproxy.fileProxyObj).length === 0 || currentItem === 0
? m('h5.bold', 'Results')
: m('table.results-container', [
m(
'thead.results-header',
m('tr', [
m('th', 'Name'),
m('th', 'Size'),
m('th', 'Hash'),
m('th', 'Download'),
])
),
m(
'tbody.results',
fproxy.fileProxyObj[currentItem.slice(1)]
? fproxy.fileProxyObj[currentItem.slice(1)].map((item) =>
m('tr', [
m('td.results__name', [m('i.fas.fa-file'), m('span', item.fName)]),
m('td.results__size', rs.formatBytes(item.fSize.xint64)),
m('td.results__hash', item.fHash),
m(
'td.results__download',
m('button', { onclick: () => handleFileDownload(item) }, 'Download')
),
])
)
: 'No Results.'
),
]),
: m('div.results-container', [
m(
'div.results-header',
m('.results-row', [
m('.results-cell.name-col', 'Name'),
m('.results-cell.size-col', 'Size'),
m('.results-cell.hash-col', 'Hash'),
m('.results-cell.action-col', 'Download'),
])
),
m(
'div.results-list',
fproxy.fileProxyObj[currentItem.slice(1)]
? fproxy.fileProxyObj[currentItem.slice(1)].map((item) =>
m('div.results-row.file-item', [
m('.results-cell.name-col', [m(getFileIcon(item.fName)), m('span', item.fName)]),
m('.results-cell.size-col', rs.formatBytes((item.fSize && (item.fSize.xint64 || item.fSize.xstr64)) || 0)),
m('.results-cell.hash-col', item.fHash),
m(
'.results-cell.action-col',
m(
'button.download-btn-v65',
{ onclick: () => handleFileDownload(item) },
'Download'
)
),
])
)
: 'No Results.'
),
]),
]),
]),
]),

View File

@ -183,9 +183,10 @@ const File = () => {
let chunkStrat;
const chunkStrats = {
// rstypes.h :: 366
0: 'Streaming', // CHUNK_STRATEGY_STREAMING
0: 'Sequential', // CHUNK_STRATEGY_SEQUENTIAL
1: 'Random', // CHUNK_STRATEGY_RANDOM
2: 'Progressive', // CHUNK_STRATEGY_PROGRESSIVE
3: 'Streaming', // CHUNK_STRATEGY_STREAMING
};
function fileCancel(hash) {
rs.rsJsonApiRequest('/rsFiles/FileCancel', { hash }).then((res) =>
@ -220,25 +221,25 @@ const File = () => {
});
}
return m('.file-view', { style: { display: info.isSearched ? 'block' : 'none' } }, [
m('.file-view__heading', [
m('.file-view__heading', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } }, [
m('h6', info.fname),
chunkStrat !== undefined &&
direction === 'down' && [
m('.file-view__heading-chunk', [
m('label[for=chunkTag]', 'Set Chunk Strategy: '),
m('select[id=chunkTag]', { value: chunkStrat, onchange: changeChunkStrategy }, [
Object.keys(chunkStrats).map((strat) =>
m('option', { value: strat }, chunkStrats[strat])
),
]),
direction === 'down' && [
m('.file-view__heading-chunk', [
m('label[for=chunkTag]', 'Set Chunk Strategy: '),
m('select[id=chunkTag]', { value: chunkStrat, onchange: changeChunkStrategy }, [
Object.keys(chunkStrats).map((strat) =>
m('option', { value: strat }, chunkStrats[strat])
),
]),
],
]),
],
]),
m('.file-view__body', [
m(
'.file-view__body-progress',
direction === 'down' &&
m(ProgressBar, { rate: (transferred / info.size.xint64) * 100, chunksInfo })
m(ProgressBar, { rate: (transferred / info.size.xint64) * 100, chunksInfo })
),
m('.file-view__body-details', [
m('.file-view__body-details-stat', [
@ -255,10 +256,10 @@ const File = () => {
`${rs.formatBytes(info.tfRate * 1024)}/s`,
]),
direction === 'down' &&
m('span', { title: 'time remaining' }, [
m('i.fas.fa-clock'),
calcRemainingTime(info.size.xint64 - transferred, info.tfRate),
]),
m('span', { title: 'time remaining' }, [
m('i.fas.fa-clock'),
calcRemainingTime(info.size.xint64 - transferred, info.tfRate),
]),
m('span', { title: 'peers' }, [m('i.fas.fa-users'), info.peers.length]),
]),
m(

View File

@ -3,6 +3,14 @@ const rs = require('rswebui');
const util = require('files/files_util');
const manager = require('files/files_manager');
const translateName = (name) => {
const n = name.toLowerCase().trim();
if (n === 'extra list' || n === '[extra list]') return 'Temporary shared files';
// Match hex strings (IDs) or pure numeric strings
if (/^[0-9a-fA-F]{16,}$/.test(name) || /^\d+$/.test(name)) return 'My Files';
return name;
};
const DisplayFiles = () => {
const childrenList = []; // stores children details
let loaded = false; // checks whether we have loaded the children details or not.
@ -15,28 +23,34 @@ const DisplayFiles = () => {
},
view: (v) => [
m('tr', [
parStruct && Object.keys(parStruct.details.children).length
parStruct && parStruct.details.children && parStruct.details.children.length
? m(
'td',
m('i.fas.fa-angle-right', {
class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`,
style: 'margin-top: 0.5rem',
onclick: () => {
if (!loaded) {
// if it is not already retrieved
parStruct.details.children.map(async (child) => {
const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {
'td',
m('i.fas.fa-angle-right', {
class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`,
style: 'margin-top: 0.5rem',
onclick: async () => {
if (!loaded) {
// if it is not already retrieved
const results = await Promise.all(
parStruct.details.children.map((child) =>
rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {
handle: child.handle.xint64,
flags: util.RS_FILE_HINTS_LOCAL,
});
})
)
);
results.forEach((res) => {
if (res && res.body && res.body.details) {
childrenList.push(res.body.details);
loaded = true;
});
}
parStruct.showChild = !parStruct.showChild;
},
})
)
}
});
loaded = true;
}
parStruct.showChild = !parStruct.showChild;
},
})
)
: m('td', ''),
m(
'td',
@ -47,29 +61,49 @@ const DisplayFiles = () => {
left: `calc(1.5rem*${v.attrs.replyDepth})`,
},
},
parStruct.details.name
translateName(parStruct.details.name || '')
),
m('td', rs.formatBytes(parStruct.details.size.xint64)),
m('td', rs.formatBytes((parStruct.details.size && parStruct.details.size.xint64) || 0)),
]),
parStruct.showChild &&
childrenList.map((child) =>
m(DisplayFiles, {
// recursive call
par_directory: { details: child, showChild: false },
replyDepth: v.attrs.replyDepth + 1,
})
),
childrenList.map((child) =>
m(DisplayFiles, {
// recursive call
par_directory: { details: child, showChild: false },
replyDepth: v.attrs.replyDepth + 1,
})
),
],
};
};
const Layout = () => {
// let root_handle;
let parent;
let showShareManager = false;
let displayList = [];
let isLoading = true;
let showShareManager = false; // Retain original declaration
return {
oninit: () => {
rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then((res) => (parent = res));
rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then(async (res) => {
if (res && res.body && res.body.details) {
if (res.body.details.name === 'root') {
// Skip root and fetch full details for each child (Location ID, Extra list, etc)
const results = await Promise.all(
res.body.details.children.map((child) =>
rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {
handle: child.handle.xint64,
flags: util.RS_FILE_HINTS_LOCAL,
})
)
);
displayList = results.map((r) => r.body.details);
} else {
displayList = [res.body.details];
}
}
isLoading = false;
m.redraw();
});
},
view: () => [
m('.widget__heading', [
@ -81,11 +115,14 @@ const Layout = () => {
util.MyFilesTable,
m(
'tbody',
parent &&
m(DisplayFiles, {
par_directory: { details: parent.body.details, showChild: false },
replyDepth: 0,
})
isLoading
? m('tr', m('td[colspan=3]', 'Loading...'))
: displayList.map((details) =>
m(DisplayFiles, {
par_directory: { details, showChild: false },
replyDepth: 0,
})
)
)
),
m(

View File

@ -2,7 +2,7 @@ const m = require('mithril');
const rs = require('rswebui');
const util = require('forums/forums_util');
const peopleUtil = require('people/people_util');
const { updatedisplayforums } = require('./forums_util');
const { updatedisplayforums, loadPostContent, getTimestampValue, formatTimestamp } = require('./forums_util');
function createforum() {
let title;
@ -32,15 +32,15 @@ function createforum() {
},
[
vnode.attrs.authorId &&
vnode.attrs.authorId.map((o) =>
m(
'option',
{ value: o },
rs.userList.userMap[o]
? rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)'
: 'No Signature'
)
),
vnode.attrs.authorId.map((o) =>
m(
'option',
{ value: o },
rs.userList.username(o)
? rs.userList.username(o) + ' (' + o.slice(0, 8) + '...)'
: 'No Signature'
)
),
]
),
m('textarea[rows=5][placeholder=Description]', {
@ -64,10 +64,10 @@ function createforum() {
res.body.retval === false
? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)])
: util.popupmessage([
m('h3', 'Success'),
m('hr'),
m('p', 'Forum created successfully'),
]);
m('h3', 'Success'),
m('hr'),
m('p', 'Forum created successfully'),
]);
},
},
'Create'
@ -95,7 +95,7 @@ const EditThread = () => {
},
[
'Identity: ',
m('h5[id=authid]', rs.userList.userMap[vnode.attrs.authorId].toLocaleString()),
m('h5[id=authid]', rs.userList.username(vnode.attrs.authorId)),
]
),
m(
@ -131,10 +131,10 @@ const EditThread = () => {
res.body.retval === false
? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)])
: util.popupmessage([
m('h3', 'Success'),
m('hr'),
m('p', 'Thread edited successfully'),
]);
m('h3', 'Success'),
m('hr'),
m('p', 'Thread edited successfully'),
]);
util.updatedisplayforums(vnode.attrs.forumId);
m.redraw();
},
@ -175,13 +175,13 @@ const AddThread = () => {
},
[
vnode.attrs.authorId &&
vnode.attrs.authorId.map((o) =>
m(
'option',
{ value: o },
rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)'
)
),
vnode.attrs.authorId.map((o) =>
m(
'option',
{ value: o },
rs.userList.username(o) + ' (' + o.slice(0, 8) + '...)'
)
),
]
),
m('textarea[rows=5]', {
@ -196,26 +196,26 @@ const AddThread = () => {
const res =
(vnode.attrs.parent_thread !== '') > 0 // is it a reply or a new thread
? await rs.rsJsonApiRequest('/rsgxsforums/createPost', {
forumId: vnode.attrs.forumId,
mBody: body,
title,
authorId: identity,
parentId: vnode.attrs.parentId,
})
forumId: vnode.attrs.forumId,
mBody: body,
title,
authorId: identity,
parentId: vnode.attrs.parentId,
})
: await rs.rsJsonApiRequest('/rsgxsforums/createPost', {
forumId: vnode.attrs.forumId,
mBody: body,
title,
authorId: identity,
});
forumId: vnode.attrs.forumId,
mBody: body,
title,
authorId: identity,
});
res.body.retval === false
? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)])
: util.popupmessage([
m('h3', 'Success'),
m('hr'),
m('p', 'Thread added successfully'),
]);
m('h3', 'Success'),
m('hr'),
m('p', 'Thread added successfully'),
]);
util.updatedisplayforums(vnode.attrs.forumId);
m.redraw();
},
@ -225,6 +225,9 @@ const AddThread = () => {
]),
};
};
// getTimestampValue and formatTimestamp are imported from forums_util.js
function displaythread() {
// recursive function to display all the threads
let groupmessagepair;
@ -255,15 +258,15 @@ function displaythread() {
[
Object.keys(parMap).length // if this thread has some replies
? m(
'td',
m('i.fas.fa-angle-right', {
class: 'fa-rotate-' + (v.attrs.threadStruct.showReplies ? '90' : '0'),
style: 'margin-top:12px',
onclick: () => {
v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies;
},
})
)
'td',
m('i.fas.fa-angle-right', {
class: 'fa-rotate-' + (v.attrs.threadStruct.showReplies ? '90' : '0'),
style: 'margin-top:12px',
onclick: () => {
v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies;
},
})
)
: m('td', ''),
m(
@ -273,124 +276,124 @@ function displaythread() {
position: 'relative',
'--replyDepth': v.attrs.replyDepth,
left: 'calc(30px*var(--replyDepth))', // shifts reply by 30 px
padding: '10px 0',
},
onclick: async () => {
v.attrs.changeThread(thread.mMeta.mOrigMsgId);
if (unread) {
const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', {
messageId: groupmessagepair,
read: true,
});
if (res.body.retval) {
updatedisplayforums(thread.mMeta.mGroupId);
m.redraw();
}
}
},
ondblclick: () =>
(v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies),
},
[
thread.mMeta.mMsgName,
m('options', { style: 'display:block' }, [
m(
'button',
{
style: 'font-size:15px',
onclick: () =>
util.popupmessage(
m(AddThread, {
parent_thread: thread.mMeta.mMsgName,
forumId: thread.mMeta.mGroupId,
authorId: v.attrs.identity,
parentId: thread.mMeta.mMsgId,
})
),
},
'Reply'
),
editpermission &&
m(
'button',
{
style: 'font-size:15px',
onclick: () =>
util.popupmessage(
m(EditThread, {
current_thread: thread.mMeta.mMsgName,
forumId: thread.mMeta.mGroupId,
current_title: thread.mMeta.mMsgName,
current_body: thread.mMsg,
authorId: thread.mMeta.mAuthorId,
current_parent: thread.mMeta.mParentId,
current_msgid: thread.mMeta.mOrigMsgId,
})
),
},
'Edit'
),
]),
]
),
m(
'td',
m(
'button',
{
style: { fontSize: '15px' },
m('div.date', { style: { fontSize: '0.8em', color: '#888' } },
formatTimestamp(thread.mMeta.mPublishTs)
),
m('div.title', {
style: { fontWeight: 'bold', fontSize: '1.1em', cursor: 'pointer', margin: '5px 0' },
onclick: async () => {
if (!unread) {
v.attrs.changeThread(thread.mMeta.mOrigMsgId);
if (unread) {
const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', {
messageId: groupmessagepair,
read: false,
read: true,
});
if (res.body.retval) {
updatedisplayforums(thread.mMeta.mGroupId);
m.redraw();
}
}
},
},
'Mark Unread'
)
),
m('td', rs.userList.userMap[thread.mMeta.mAuthorId]),
m(
'td',
typeof thread.mMeta.mPublishTs === 'object'
? new Date(thread.mMeta.mPublishTs.xint64 * 1000).toLocaleString()
: 'undefined'
ondblclick: () =>
(v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies),
}, [
thread.mMeta.mMsgName,
m('options', { style: 'display:block; margin-top: 5px;' }, [
m(
'button',
{
style: 'font-size:12px; margin-right: 5px;',
onclick: (e) => {
e.stopPropagation();
util.popupmessage(
m(AddThread, {
parent_thread: thread.mMeta.mMsgName,
forumId: thread.mMeta.mGroupId,
authorId: v.attrs.identity,
parentId: thread.mMeta.mMsgId,
})
);
},
},
'Reply'
),
editpermission &&
m(
'button',
{
style: 'font-size:12px; margin-right: 5px;',
onclick: async (e) => {
e.stopPropagation();
const body = await loadPostContent(
thread.mMeta.mGroupId,
thread.mMeta.mOrigMsgId
);
util.popupmessage(
m(EditThread, {
current_thread: thread.mMeta.mMsgName,
forumId: thread.mMeta.mGroupId,
current_title: thread.mMeta.mMsgName,
current_body: body || '',
authorId: thread.mMeta.mAuthorId,
current_parent: thread.mMeta.mParentId,
current_msgid: thread.mMeta.mOrigMsgId,
})
);
},
},
'Edit'
),
m(
'button',
{
style: { fontSize: '12px' },
onclick: async (e) => {
e.stopPropagation();
const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', {
messageId: groupmessagepair,
read: !unread ? true : false,
});
if (res.body.retval) {
updatedisplayforums(thread.mMeta.mGroupId);
m.redraw();
}
},
},
unread ? 'Mark Read' : 'Mark Unread'
),
]),
]),
m('div.author', { style: { fontSize: '0.9em', fontStyle: 'italic' } }, rs.userList.username(thread.mMeta.mAuthorId)),
]
),
]
),
v.attrs.threadStruct.showReplies &&
Object.keys(parMap).map((key, index) =>
m(displaythread, {
// recursive call to all replies
threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mOrigMsgId],
replyDepth: v.attrs.replyDepth + 1,
identity: v.attrs.identity,
changeThread: v.attrs.changeThread,
})
),
Object.keys(parMap).map((key, index) =>
m(displaythread, {
// recursive call to all replies
threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mOrigMsgId],
replyDepth: v.attrs.replyDepth + 1,
identity: v.attrs.identity,
changeThread: v.attrs.changeThread,
})
),
];
},
};
}
const ThreadView = () => {
let thread = {};
let ownId;
return {
showThread: '',
oninit: async (v) => {
if (
util.Data.ParentThreads[v.attrs.forumId] &&
util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId]
) {
thread = util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId];
}
oninit: (v) => {
util.updatedisplayforums(v.attrs.forumId);
peopleUtil.ownIds((data) => {
ownId = data;
for (let i = 0; i < ownId.length; i++) {
@ -400,76 +403,94 @@ const ThreadView = () => {
}
});
},
view: (v) =>
m('.widget', { key: v.attrs.msgId }, [
view: (v) => {
const forumId = v.attrs.forumId;
const msgId = v.attrs.msgId;
const threadStruct = (util.Data.Threads[forumId] && util.Data.Threads[forumId][msgId]) ? util.Data.Threads[forumId][msgId] : null;
if (!threadStruct) {
return m('.widget', [
m(
'a[title=Back]',
{
onclick: () => m.route.set('/forums/:tab/:mGroupId', {
tab: m.route.param().tab,
mGroupId: forumId,
}),
},
m('i.fas.fa-arrow-left')
),
m('h3', 'Loading...'),
]);
}
const meta = threadStruct.thread.mMeta;
const unread = meta.mMsgStatus === util.THREAD_UNREAD;
return m('.widget', { key: msgId }, [
m(
'a[title=Back]',
{
onclick: () =>
m.route.set('/forums/:tab/:mGroupId', {
tab: m.route.param().tab,
mGroupId: m.route.param().mGroupId,
}),
onclick: () => m.route.set('/forums/:tab/:mGroupId', {
tab: m.route.param().tab,
mGroupId: forumId,
}),
},
m('i.fas.fa-arrow-left')
),
m('h3', thread.mMsgName),
m('div.post-header', { style: { margin: '10px 0' } }, [
m('div.date', { style: { color: '#888', fontSize: '0.9em' } }, formatTimestamp(meta.mPublishTs)),
m('h4.title', { style: { margin: '5px 0', fontWeight: 'bold' } }, meta.mMsgName),
m('div.author', { style: { fontStyle: 'italic', fontSize: '1em' } }, rs.userList.username(meta.mAuthorId)),
]),
m('hr'),
m(
util.ThreadsReplyTable,
m(
'tbody',
util.Data.Threads[v.attrs.forumId] &&
util.Data.Threads[v.attrs.forumId][v.attrs.msgId] &&
m(displaythread, {
threadStruct: util.Data.Threads[v.attrs.forumId][v.attrs.msgId],
replyDepth: 0,
identity: ownId,
changeThread(newThread) {
v.state.showThread = newThread;
// For displaying the messages of the threads. We pass this into the recursive function displaythreads()
},
})
)
),
m('hr'),
v.state.showThread && [
m('h4', 'Messages'),
util.Data.Threads[v.attrs.forumId] &&
util.Data.Threads[v.attrs.forumId][v.state.showThread] &&
m('p', m.trust(util.Data.Threads[v.attrs.forumId][v.state.showThread].thread.mMsg)),
// m.trust is to render html content directly.
],
]),
m('div.actions', { style: { marginBottom: '15px' } }, [
m('button', {
style: { marginRight: '10px' },
onclick: () => util.popupmessage(m(AddThread, {
parent_thread: meta.mMsgName,
forumId: forumId,
authorId: ownId,
parentId: msgId,
}))
}, 'Reply'),
m('button', {
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', {
messageId: { first: forumId, second: meta.mOrigMsgId },
read: !unread,
});
if (res.body.retval) {
util.updatedisplayforums(forumId);
m.redraw();
}
}
}, unread ? 'Mark Read' : 'Mark Unread'),
]),
m('div.content', {
style: {
width: '100%',
backgroundColor: '#f9f9f9',
padding: '15px',
borderRadius: '5px',
whiteSpace: 'pre-wrap', // Preserve line breaks
wordBreak: 'break-word',
}
}, [
threadStruct.thread.mMsg !== null
? m.trust(threadStruct.thread.mMsg)
: (loadPostContent(forumId, msgId), m('p', 'Loading content...'))
]),
]);
},
};
};
const ForumView = () => {
let fname = '';
let fauthor = '';
let fsubscribed = {};
let createDate = {};
let lastActivity = {};
let topThreads = {};
let ownId = '';
return {
oninit: (v) => {
if (util.Data.DisplayForums[v.attrs.id]) {
fname = util.Data.DisplayForums[v.attrs.id].name;
fsubscribed = util.Data.DisplayForums[v.attrs.id].isSubscribed;
createDate = util.Data.DisplayForums[v.attrs.id].created;
lastActivity = util.Data.DisplayForums[v.attrs.id].activity;
if (rs.userList.userMap[util.Data.DisplayForums[v.attrs.id].author]) {
fauthor = rs.userList.userMap[util.Data.DisplayForums[v.attrs.id].author];
} else if (Number(util.Data.DisplayForums[v.attrs.id].author) === 0) {
fauthor = 'No Contact Author';
} else {
fauthor = 'Unknown';
}
}
if (util.Data.ParentThreads[v.attrs.id]) {
topThreads = util.Data.ParentThreads[v.attrs.id];
}
util.updatedisplayforums(v.attrs.id);
peopleUtil.ownIds((data) => {
ownId = data;
for (let i = 0; i < ownId.length; i++) {
@ -479,117 +500,133 @@ const ForumView = () => {
}
});
},
view: (v) => [
m(
'a[title=Back]',
{
onclick: () =>
m.route.set('/forums/:tab', {
tab: m.route.param().tab,
}),
},
m('i.fas.fa-arrow-left')
),
view: (v) => {
const forumDetails = util.Data.DisplayForums[v.attrs.id] || {
name: 'Loading...',
isSubscribed: false,
created: {},
activity: {},
author: '0',
description: 'Loading...',
};
const allPosts = util.Data.Threads[v.attrs.id]
? Object.values(util.Data.Threads[v.attrs.id]).map((ts) => ts.thread.mMeta)
: [];
const fname = forumDetails.name;
const fsubscribed = forumDetails.isSubscribed;
const createDate = forumDetails.created;
const lastActivity = forumDetails.activity;
let fauthor = 'Unknown';
m('h3', fname),
m(
'button',
{
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsgxsforums/subscribeToForum', {
forumId: v.attrs.id,
subscribe: !fsubscribed,
});
if (res.body.retval) {
fsubscribed = !fsubscribed;
util.Data.DisplayForums[v.attrs.id].isSubscribed = fsubscribed;
}
if (rs.userList.userMap[forumDetails.author]) {
fauthor = rs.userList.userMap[forumDetails.author];
} else if (Number(forumDetails.author) === 0) {
fauthor = 'No Contact Author';
}
return [
m(
'a[title=Back]',
{
onclick: () =>
m.route.set('/forums/:tab', {
tab: m.route.param().tab,
}),
},
},
fsubscribed ? 'Subscribed' : 'Subscribe'
),
m('[id=forumdetails]', [
m(
'p',
m('b', 'Date created: '),
typeof createDate === 'object'
? new Date(createDate.xint64 * 1000).toLocaleString()
: 'undefined'
m('i.fas.fa-arrow-left')
),
m('p', m('b', 'Admin: '), fauthor),
m(
'p',
m('b', 'Last activity: '),
typeof lastActivity === 'object'
? new Date(lastActivity.xint64 * 1000).toLocaleString()
: 'undefined'
),
]),
m('hr'),
m('forumdesc', m('b', 'Description: '), util.Data.DisplayForums[v.attrs.id].description),
m('hr'),
m(
'threaddetails',
{
style: 'display:' + (fsubscribed ? 'block' : 'none'),
},
m('h3', 'Threads'),
m('h3', fname),
m(
'button',
{
onclick: () => {
util.popupmessage(
m(AddThread, {
parent_thread: '',
forumId: v.attrs.id,
authorId: ownId,
parentId: '',
})
);
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsgxsforums/subscribeToForum', {
forumId: v.attrs.id,
subscribe: !fsubscribed,
});
if (res.body.retval) {
util.Data.DisplayForums[v.attrs.id].isSubscribed = !fsubscribed;
}
},
},
['New Thread', m('i.fas.fa-pencil-alt')]
fsubscribed ? 'Subscribed' : 'Subscribe'
),
m('[id=forumdetails]', [
m(
'p',
m('b', 'Date created: '),
formatTimestamp(createDate)
),
m('p', m('b', 'Admin: '), fauthor),
m(
'p',
m('b', 'Last activity: '),
formatTimestamp(lastActivity)
),
]),
m('hr'),
m('forumdesc', m('b', 'Description: '), forumDetails.description),
m('hr'),
m(
util.ThreadsTable,
'threaddetails',
{
style: 'display:' + (fsubscribed ? 'block' : 'none'),
},
m('h3', 'Threads'),
m(
'tbody',
Object.keys(topThreads).map((key, index) =>
m(
'tr',
{
style:
topThreads[key].mMsgStatus === util.THREAD_UNREAD ? { fontWeight: 'bold' } : '',
onclick: () => {
m.route.set('/forums/:tab/:mGroupId/:mMsgId', {
tab: m.route.param().tab,
mGroupId: v.attrs.id,
mMsgId: topThreads[key].mOrigMsgId,
});
},
},
[
m('td', topThreads[key].mMsgName),
'button',
{
onclick: () => {
util.popupmessage(
m(AddThread, {
parent_thread: '',
forumId: v.attrs.id,
authorId: ownId,
parentId: '',
})
);
},
},
['New Thread', m('i.fas.fa-pencil-alt')]
),
m('hr'),
m(
util.ThreadsTable,
m(
'tbody',
allPosts
.sort((a, b) => getTimestampValue(b.mPublishTs) - getTimestampValue(a.mPublishTs))
.map((thread) =>
m(
'td',
typeof topThreads[key].mPublishTs === 'object'
? new Date(topThreads[key].mPublishTs.xint64 * 1000).toLocaleString()
: 'undefined'
),
m(
'td',
rs.userList.userMap[topThreads[key].mAuthorId]
? rs.userList.userMap[topThreads[key].mAuthorId]
: 'Unknown'
),
]
)
'tr',
{
style:
thread.mMsgStatus === util.THREAD_UNREAD ? { fontWeight: 'bold' } : '',
},
m('td', { style: { padding: '10px 0' } }, [
m('div.date', { style: { fontSize: '0.8em', color: '#888' } },
formatTimestamp(thread.mPublishTs)
),
m('div.title', {
style: { fontWeight: 'bold', fontSize: '1.2em', cursor: 'pointer', margin: '5px 0' },
onclick: () => {
m.route.set('/forums/:tab/:mGroupId/:mMsgId', {
tab: m.route.param().tab,
mGroupId: v.attrs.id,
mMsgId: thread.mOrigMsgId,
});
},
}, thread.mMsgName),
m('div.author', { style: { fontSize: '0.9em', fontStyle: 'italic' } }, rs.userList.username(thread.mAuthorId)),
])
)
)
)
)
)
),
],
),
];
},
};
};

View File

@ -12,16 +12,18 @@ const getForums = {
MyForums: [],
async load() {
const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsSummaries');
getForums.All = res.body.forums;
getForums.PopularForums = getForums.All;
getForums.SubscribedForums = getForums.All.filter(
(forum) =>
forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED ||
forum.mSubscribeFlags === util.GROUP_MY_FORUM
);
getForums.MyForums = getForums.All.filter(
(forum) => forum.mSubscribeFlags === util.GROUP_MY_FORUM
);
if (res && res.body && res.body.forums) {
getForums.All = res.body.forums;
getForums.PopularForums = getForums.All;
getForums.SubscribedForums = getForums.All.filter(
(forum) =>
forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED ||
forum.mSubscribeFlags === util.GROUP_MY_FORUM
);
getForums.MyForums = getForums.All.filter(
(forum) => forum.mSubscribeFlags === util.GROUP_MY_FORUM
);
}
},
};
const sections = {
@ -37,7 +39,7 @@ const Layout = () => {
return {
oninit: () => {
rs.setBackgroundTask(getForums.load, 5000, () => {
// return m.route.get() === '/files/files';
return m.route.get().includes('/forums');
});
peopleUtil.ownIds((data) => {
ownId = data;
@ -52,6 +54,7 @@ const Layout = () => {
view: (vnode) =>
m('.widget', [
m('.top-heading', [
vnode.attrs.pathInfo.tab === 'MyForums' &&
m(
'button',
{
@ -71,14 +74,14 @@ const Layout = () => {
]),
Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // thread's view
? m(viewUtil.ThreadView, {
msgId: vnode.attrs.pathInfo.mMsgId,
forumId: vnode.attrs.pathInfo.mGroupId,
})
msgId: vnode.attrs.pathInfo.mMsgId,
forumId: vnode.attrs.pathInfo.mGroupId,
})
: Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // Forum's view
? m(viewUtil.ForumView, {
? m(viewUtil.ForumView, {
id: vnode.attrs.pathInfo.mGroupId,
})
: m(sections[vnode.attrs.pathInfo.tab], {
: m(sections[vnode.attrs.pathInfo.tab], {
list: getForums[vnode.attrs.pathInfo.tab],
}),
]),

View File

@ -14,76 +14,166 @@ const Data = {
Threads: {},
ParentThreads: {},
ParentThreadMap: {},
loading: new Set(),
};
async function updatedisplayforums(keyid, details = {}) {
const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', {
forumIds: [keyid], // keyid: Forumid
});
details = res.body.forumsInfo[0];
Data.DisplayForums[keyid] = {
// struct for a forum
name: details.mMeta.mGroupName,
author: details.mMeta.mAuthorId,
isSearched: true,
description: details.mDescription,
isSubscribed:
details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED ||
details.mMeta.mSubscribeFlags === GROUP_MY_FORUM,
activity: details.mMeta.mLastPost,
created: details.mMeta.mPublishTs,
};
if (Data.Threads[keyid] === undefined) {
Data.Threads[keyid] = {};
function getTimestampValue(ts) {
if (!ts) return 0;
if (typeof ts === 'object') {
if (ts.xint64 !== undefined) return ts.xint64;
if (ts.xstr64 !== undefined) return Number(ts.xstr64);
return 0;
}
const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumMsgMetaData', {
forumId: keyid,
});
if (res2.body.retval) {
res2.body.msgMetas.map(async (thread) => {
const res3 = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', {
forumId: keyid,
msgsIds: [thread.mMsgId],
return ts;
}
function formatTimestamp(ts) {
const val = getTimestampValue(ts);
if (!val || val === 0) return '???';
try {
const localDate = new Date(val * 1000);
const offset = localDate.getTimezoneOffset() * 60000;
return new Date(localDate.getTime() - offset).toISOString().replace('T', ' ').slice(0, 16);
} catch (e) {
return 'Invalid Date';
}
}
async function updatedisplayforums(keyid) {
if (Data.loading.has(keyid)) return;
Data.loading.add(keyid);
try {
const res1 = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', {
forumIds: [keyid], // keyid: Forumid
});
if (res1 && res1.body && res1.body.retval && res1.body.forumsInfo && res1.body.forumsInfo.length > 0) {
const forumInfo = res1.body.forumsInfo[0];
Data.DisplayForums[keyid] = {
// struct for a forum
name: forumInfo.mMeta.mGroupName,
author: forumInfo.mMeta.mAuthorId,
isSearched: true,
description: forumInfo.mDescription,
isSubscribed:
forumInfo.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED ||
forumInfo.mMeta.mSubscribeFlags === GROUP_MY_FORUM,
activity: forumInfo.mMeta.mLastPost,
created: forumInfo.mMeta.mPublishTs,
};
if (Data.Threads[keyid] === undefined) {
Data.Threads[keyid] = {};
}
const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumPostsHierarchy', {
group: forumInfo,
});
if (
res3.body.retval &&
(Data.Threads[keyid][thread.mOrigMsgId] === undefined ||
Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mPublishTs.xint64 <
thread.mPublishTs.xint64)
// here we get the latest edited thread for each thread by comparing the publish time
) {
Data.Threads[keyid][thread.mOrigMsgId] = { thread: res3.body.msgs[0], showReplies: false };
if (
Data.Threads[keyid][thread.mOrigMsgId] &&
Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mMsgStatus === THREAD_UNREAD
) {
let parent = Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mParentId;
while (Data.Threads[keyid][parent]) {
// to mark all parent threads of an inread thread
Data.Threads[keyid][parent].thread.mMeta.mMsgStatus = THREAD_UNREAD;
parent = Data.Threads[keyid][parent].thread.mMeta.mParentId;
}
}
if (res2 && res2.body && res2.body.vect) {
const vect = res2.body.vect;
// Index 0 is the root sentinel in GXS hierarchy
const rootSentinel = vect[0];
if (Data.ParentThreads[keyid] === undefined) {
if (rootSentinel && rootSentinel.mChildren) {
Data.ParentThreads[keyid] = {};
}
if (thread.mThreadId === thread.mParentId) {
// top level thread.
Data.ParentThreads[keyid][thread.mOrigMsgId] =
Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta;
} else {
if (Data.ParentThreadMap[thread.mParentId] === undefined) {
Data.ParentThreadMap[thread.mParentId] = {};
}
Data.ParentThreadMap[thread.mParentId][thread.mOrigMsgId] = thread;
rootSentinel.mChildren.forEach((topIndex) => {
const EntryToThread = (entryIndex) => {
const entry = vect[entryIndex];
const replies = {};
// Map ForumPostEntry to a structure compatible with the existing UI
const meta = {
mGroupId: keyid,
mMsgId: entry.mMsgId,
mOrigMsgId: entry.mMsgId,
mThreadId: entry.mMsgId,
mParentId:
entry.mParent !== 0
? vect[entry.mParent].mMsgId
: '00000000000000000000000000000000',
mAuthorId: entry.mAuthorId,
mMsgName: entry.mTitle,
mPublishTs: entry.mPublishTs,
mMostRecentTsInThread: getTimestampValue(entry.mPublishTs),
mMsgStatus: entry.mMsgStatus,
};
// Populate ParentThreadMap for compatibility
if (meta.mParentId !== '00000000000000000000000000000000') {
if (!Data.ParentThreadMap[meta.mParentId]) Data.ParentThreadMap[meta.mParentId] = {};
Data.ParentThreadMap[meta.mParentId][meta.mMsgId] = meta;
}
const threadStruct = {
thread: { mMeta: meta, mMsg: null },
replies: replies,
showReplies: false,
};
// Add to flat map
Data.Threads[keyid][meta.mMsgId] = threadStruct;
if (entry.mChildren) {
entry.mChildren.forEach((childIndex) => {
const childThread = EntryToThread(childIndex);
replies[childThread.thread.mMeta.mMsgId] = childThread;
const childTs = childThread.thread.mMeta.mMostRecentTsInThread || 0;
if (childTs > meta.mMostRecentTsInThread) meta.mMostRecentTsInThread = childTs;
});
}
return threadStruct;
};
const topThread = EntryToThread(topIndex);
Data.ParentThreads[keyid][topThread.thread.mMeta.mMsgId] = topThread.thread.mMeta;
});
}
}
});
m.redraw();
}
} catch (e) {
console.error('[RS] Error updating forum display for:', keyid, e);
} finally {
Data.loading.delete(keyid);
m.redraw(); // Final redraw just in case
}
}
/**
* Load the body (mMsg) of a single forum post on demand.
* Returns a Promise that resolves to the body string, or null on failure.
*/
async function loadPostContent(forumId, msgId) {
// If body is already loaded, return it immediately
if (
Data.Threads[forumId] &&
Data.Threads[forumId][msgId] &&
Data.Threads[forumId][msgId].thread.mMsg !== null
) {
return Data.Threads[forumId][msgId].thread.mMsg;
}
try {
const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', {
forumId: forumId,
msgsIds: [msgId],
});
if (res && res.body && res.body.retval && res.body.msgs && res.body.msgs.length > 0) {
const body = res.body.msgs[0].mMsg;
// Cache the body in the existing thread entry
if (Data.Threads[forumId] && Data.Threads[forumId][msgId]) {
Data.Threads[forumId][msgId].thread.mMsg = body;
}
m.redraw();
return body;
}
} catch (e) {
console.error('[RS] Error loading post content:', forumId, msgId, e);
}
return null;
}
const DisplayForumsFromList = () => {
return {
view: (v) =>
@ -114,7 +204,7 @@ const ForumSummary = () => {
keyid = v.attrs.details.mGroupId;
updatedisplayforums(keyid);
},
view: (v) => {},
view: (v) => { },
};
};
@ -125,26 +215,18 @@ const ForumTable = () => {
};
const ThreadsTable = () => {
return {
oninit: (v) => {},
oninit: (v) => { },
view: (v) =>
m('table.threads', [
m('tr', [m('th', 'Comment'), m('th', 'Date'), m('th', 'Author')]),
v.children,
]),
};
};
const ThreadsReplyTable = () => {
return {
oninit: (v) => {},
oninit: (v) => { },
view: (v) =>
m('table.threadreply', [
m('tr', [
m('th', ''),
m('th', 'Comment'),
m('th', 'Unread'),
m('th', 'Author'),
m('th', 'Date'),
]),
v.children,
]),
};
@ -197,6 +279,9 @@ module.exports = {
ThreadsReplyTable,
popupmessage,
updatedisplayforums,
loadPostContent,
getTimestampValue,
formatTimestamp,
GROUP_SUBSCRIBE_ADMIN,
GROUP_SUBSCRIBE_NOT_SUBSCRIBED,
GROUP_SUBSCRIBE_PUBLISH,

View File

@ -117,52 +117,52 @@ function confirmAddPrompt(details, cert, long) {
long
? m(
'button',
{
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert });
if (res.body.retval) {
widget.popupMessage([
m('h3', 'Successful'),
m('hr'),
m('p', 'Successfully added friend.'),
]);
} else {
widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', 'An error occoured during adding. Friend not added.'),
]);
}
},
'button',
{
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert });
if (res.body.retval) {
widget.popupMessage([
m('h3', 'Successful'),
m('hr'),
m('p', 'Successfully added friend.'),
]);
} else {
widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', 'An error occoured during adding. Friend not added.'),
]);
}
},
'Finish'
)
},
'Finish'
)
: m(
'button',
{
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsPeers/addSslOnlyFriend', {
sslId: details.id,
pgpId: details.gpg_id,
});
if (res.body.retval) {
widget.popupMessage([
m('h3', 'Successful'),
m('hr'),
m('p', 'Successfully added friend.'),
]);
} else {
widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', 'An error occoured during adding. Friend not added.'),
]);
}
},
'button',
{
onclick: async () => {
const res = await rs.rsJsonApiRequest('/rsPeers/addSslOnlyFriend', {
sslId: details.id,
pgpId: details.gpg_id,
});
if (res.body.retval) {
widget.popupMessage([
m('h3', 'Successful'),
m('hr'),
m('p', 'Successfully added friend.'),
]);
} else {
widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', 'An error occoured during adding. Friend not added.'),
]);
}
},
'Finish'
),
},
'Finish'
),
]);
}
@ -170,7 +170,7 @@ async function addFriendFromCert(cert) {
const res = await rs.rsJsonApiRequest('/rsPeers/parseShortInvite', { invite: cert });
if (res.body.retval) {
console.log(res.body);
// console.log(res.body);
confirmAddPrompt(res.body.details, cert, false);
} else {
rs.rsJsonApiRequest('/rsPeers/loadDetailsFromStringCert', { cert }, (data) => {

View File

@ -16,7 +16,7 @@ const verifyLogin = async function (uname, passwd, url, displayAuthError = true)
rs.setKeys('', '', url, false);
rs.logon(
loginHeader,
displayAuthError ? displayErrorMessage : () => {},
displayAuthError ? displayErrorMessage : () => { },
displayErrorMessage,
() => {
rs.setKeys(uname, passwd, url);
@ -33,9 +33,9 @@ function loginComponent() {
urlParams.get('Url') || window.location.protocol === 'file:'
? 'http://127.0.0.1:9092'
: window.location.protocol +
'//' +
window.location.host +
window.location.pathname.replace('/index.html', '');
'//' +
window.location.host +
window.location.pathname.replace('/index.html', '');
let withOptions = false;
const logo = () =>
@ -83,7 +83,13 @@ function loginComponent() {
m('a', { onclick: () => (withOptions = !withOptions) }, `${action} options`);
const textError = () => m('p.error[id=error]');
return {
oninit: () => {
if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) {
verifyLogin(rs.loginKey.username, rs.loginKey.passwd, rs.loginKey.url, false);
}
},
view: () => {
return m(
'form.login-page',
@ -91,14 +97,14 @@ function loginComponent() {
'.login-container',
withOptions
? [
logo(),
m('.extra', [m('label', 'Username:'), m('br'), inputName()]),
m('.extra', [m('label', 'Password:'), m('br'), inputPassword()]),
m('.extra', [m('label', 'Url:'), m('br'), inputUrl()]),
linkOptions('hide'),
buttonLogin(),
textError(),
]
logo(),
m('.extra', [m('label', 'Username:'), m('br'), inputName()]),
m('.extra', [m('label', 'Password:'), m('br'), inputPassword()]),
m('.extra', [m('label', 'Url:'), m('br'), inputUrl()]),
linkOptions('hide'),
buttonLogin(),
textError(),
]
: [logo(), inputPassword(), linkOptions('show'), buttonLogin(), textError()]
)
);

View File

@ -21,33 +21,27 @@ const Messages = {
later: [],
load() {
rs.rsJsonApiRequest('/rsMail/getMessageSummaries', { box: util.BOX_ALL }, (data) => {
Messages.all = data.msgList;
Messages.inbox = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_INBOX
);
Messages.sent = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_SENTBOX
);
Messages.outbox = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_OUTBOX
);
Messages.drafts = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX
);
Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH);
Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR);
Messages.system = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SYSTEM);
Messages.spam = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SPAM);
if (data && data.msgList) {
Messages.all = data.msgList;
Messages.inbox = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_INBOX
);
Messages.sent = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_SENTBOX
);
Messages.outbox = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_OUTBOX
);
Messages.drafts = Messages.all.filter(
(msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX
);
Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH);
Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR);
Messages.system = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SYSTEM);
Messages.spam = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SPAM);
Messages.attachment = Messages.all.filter((msg) => msg.count);
// Messages.important = Messages.all.filter(
// (msg) => msg.msgflags & util.RS_MSGTAGTYPE_IMPORTANT
// );
// Messages.work = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_WORK);
// Messages.personal = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_PERSONAL);
// Messages.todo = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_TODO);
// Messages.later = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_LATER);
Messages.attachment = Messages.all.filter((msg) => msg.count);
}
});
},
};
@ -84,22 +78,22 @@ const Layout = () => {
oninit: () => Messages.load(),
view: (vnode) => {
const sectionsSize = {
inbox: Messages.inbox.length,
outbox: Messages.outbox.length,
drafts: Messages.drafts.length,
sent: Messages.sent.length,
trash: Messages.trash.length,
inbox: (Messages.inbox || []).length,
outbox: (Messages.outbox || []).length,
drafts: (Messages.drafts || []).length,
sent: (Messages.sent || []).length,
trash: (Messages.trash || []).length,
};
const sectionsQuickviewSize = {
starred: Messages.starred.length,
system: Messages.system.length,
spam: Messages.spam.length,
attachment: Messages.attachment.length,
important: Messages.important.length,
work: Messages.work.length,
todo: Messages.todo.length,
later: Messages.later.length,
personal: Messages.personal.length,
starred: (Messages.starred || []).length,
system: (Messages.system || []).length,
spam: (Messages.spam || []).length,
attachment: (Messages.attachment || []).length,
important: (Messages.important || []).length,
work: (Messages.work || []).length,
todo: (Messages.todo || []).length,
later: (Messages.later || []).length,
personal: (Messages.personal || []).length,
};
return [
@ -120,17 +114,17 @@ const Layout = () => {
'.node-panel',
m('.widget', [
m.route.get().split('/').length < 4 &&
m('.top-heading', [
m(
'select.mail-tag',
{
value: tagselect.showval,
onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]),
},
[tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))]
),
m(util.SearchBar, { list: {} }),
]),
m('.top-heading', [
m(
'select.mail-tag',
{
value: tagselect.showval,
onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]),
},
[tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))]
),
m(util.SearchBar, { list: {} }),
]),
vnode.children,
])
),
@ -157,9 +151,9 @@ module.exports = {
return m(
Layout,
m(sections[tab] || sectionsquickview[tab], {
list: Messages[tab].sort((msgA, msgB) => {
const msgADate = new Date(msgA.ts.xint64 * 1000);
const msgBDate = new Date(msgB.ts.xint64 * 1000);
list: (Messages[tab] || []).sort((msgA, msgB) => {
const msgADate = new Date((msgA.ts.xint64 || 0) * 1000);
const msgBDate = new Date((msgB.ts.xint64 || 0) * 1000);
return msgADate < msgBDate;
}),
})

View File

@ -128,7 +128,7 @@ const AttachmentSection = () => {
m('i.fas.fa-file-medical'),
m('h3', `File is ${status.retval ? 'being' : 'already'} downloaded!`),
])
).catch((error) => console.log('error: ', error));
).catch((error) => { });
}
return {
view: (v) =>
@ -251,69 +251,69 @@ const MessageView = () => {
m('h3', MailData.subject),
m('.msg-details', [
MailData.sender &&
m(peopleUtil.UserAvatar, {
avatar: MailData.avatar,
firstLetter: rs.userList.userMap[MailData.sender._addr_string]
? rs.userList.userMap[MailData.sender._addr_string].slice(0, 1).toUpperCase()
: '',
}),
m(peopleUtil.UserAvatar, {
avatar: MailData.avatar,
firstLetter: rs.userList.userMap[MailData.sender._addr_string]
? rs.userList.userMap[MailData.sender._addr_string].slice(0, 1).toUpperCase()
: '',
}),
m('.msg-details__info', [
MailData.sender &&
m('.msg-details__info-item', [
m('b', 'From: '),
rs.userList.userMap[MailData.sender._addr_string] || 'Unknown',
]),
m('.msg-details__info-item', [
m('b', 'From: '),
rs.userList.userMap[MailData.sender._addr_string] || 'Unknown',
]),
m('.msg-details__info-item', [
m('b', 'To: '),
MailData.toList && Object.keys(MailData.toList).length > 0
? [
m('#truncate.truncated-view', [
Object.keys(MailData.toList).map((key, index) =>
m('span', { key: index }, `${rs.userList.userMap[key] || 'Unknown'}, `)
),
]),
m(
'button.toggle-truncate',
{
style: {
display: Object.keys(MailData.toList).length > 10 ? 'block' : 'none',
},
onclick: () => {
document
.querySelector('#truncate')
.classList.toggle('truncated-view');
},
},
'...'
m('#truncate.truncated-view', [
Object.keys(MailData.toList).map((key, index) =>
m('span', { key: index }, `${rs.userList.userMap[key] || 'Unknown'}, `)
),
]
]),
m(
'button.toggle-truncate',
{
style: {
display: Object.keys(MailData.toList).length > 10 ? 'block' : 'none',
},
onclick: () => {
document
.querySelector('#truncate')
.classList.toggle('truncated-view');
},
},
'...'
),
]
: m('span', 'Unknown'),
]),
MailData.ccList &&
Object.keys(MailData.ccList).length > 0 &&
m('.msg-details__info-item', [
m('b', 'Cc: '),
Object.keys(MailData.ccList).map((key, index) =>
m('p', { key: index }, `${rs.userList.userMap[key]}, `)
),
]),
Object.keys(MailData.ccList).length > 0 &&
m('.msg-details__info-item', [
m('b', 'Cc: '),
Object.keys(MailData.ccList).map((key, index) =>
m('p', { key: index }, `${rs.userList.userMap[key]}, `)
),
]),
MailData.bccList &&
Object.keys(MailData.bccList).length > 0 &&
m('.msg-details__info-item', [
m('b', 'Bcc: '),
Object.keys(MailData.bccList).map((key, index) =>
m('p', { key: index }, `${rs.userList.userMap[key]}, `)
),
]),
Object.keys(MailData.bccList).length > 0 &&
m('.msg-details__info-item', [
m('b', 'Bcc: '),
Object.keys(MailData.bccList).map((key, index) =>
m('p', { key: index }, `${rs.userList.userMap[key]}, `)
),
]),
]),
]),
]),
m('.msg-view__body', m('#msgView')),
MailData.files.length > 0 &&
m('.msg-view__attachment', [
m('h3', 'Attachments'),
m('.msg-view__attachment-items', m(AttachmentSection, { files: MailData.files })),
]),
m('.msg-view__attachment', [
m('h3', 'Attachments'),
m('.msg-view__attachment-items', m(AttachmentSection, { files: MailData.files })),
]),
],
m(
'.composePopupOverlay#mailComposerPopup',
@ -322,14 +322,14 @@ const MessageView = () => {
'.composePopup',
MailData.sender._addr_string
? m(compose, {
msgType: 'reply',
senderId: MailData.sender._addr_string,
recipientList: MailData.toList,
subject: MailData.subject,
replyMessage: MailData.message,
timeStamp: new Date(MailData.timeStamp * 1000),
setShowCompose,
})
msgType: 'reply',
senderId: MailData.sender._addr_string,
recipientList: MailData.toList,
subject: MailData.subject,
replyMessage: MailData.message,
timeStamp: new Date(MailData.timeStamp * 1000),
setShowCompose,
})
: m('.widget', m('.widget__heading', m('h3', 'Sender is not known'))),
m('button.red.close-btn', { onclick: () => setShowCompose(false) }, m('i.fas.fa-times'))
)

View File

@ -1,6 +1,7 @@
const m = require('mithril');
const login = require('login');
const rs = require('rswebui');
const home = require('home');
const network = require('network/network');
const people = require('people/people_resolver');
@ -36,24 +37,65 @@ const navbar = () => {
},
[
m('.nav-menu__logo', [
m('img', {
src: 'images/retroshare.svg',
alt: 'retroshare_icon',
}),
m('h5', 'Retroshare'),
m(
'.logo-container',
{
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginRight: '10px',
},
},
[
m('img', {
src: 'images/retroshare.svg',
alt: 'retroshare_icon',
}),
m('i.fas.fa-circle', {
style: {
color: rs.connectionState.status ? '#2ecc71' : '#e74c3c',
fontSize: '0.6em',
marginTop: '5px',
transition: 'color 0.3s ease',
},
title: rs.connectionState.status ? 'Connected to RetroShare Core' : 'Connection Lost',
}),
m('span.webui-version', { style: { fontSize: '0.7em', marginTop: '3px', color: '#888' } }, 'v131'),
m('i.fas.fa-sync-alt.refresh-icon', {
style: { fontSize: '0.8em', marginTop: '2px', cursor: 'pointer', color: '#888' },
onclick: () => window.location.reload(true),
title: 'Force reload application',
}),
]
),
m('.nav-menu__logo-text', [
m('h5', 'RetroShare'),
]),
]),
m('.nav-menu__box', [
Object.keys(vnode.attrs.links).map((linkName, i) => {
Object.keys(vnode.attrs.links).map((linkName) => {
const active = m.route.get().split('/')[1] === linkName;
return m(
m.route.Link,
{
href: vnode.attrs.links[linkName],
class: 'item' + (active ? ' item-selected' : ''),
class: (active ? 'active-link' : '') + ' item',
},
[navIcon[linkName], m('p', linkName)]
[
navIcon[linkName],
m('span', linkName.charAt(0).toUpperCase() + linkName.slice(1)),
]
);
}),
m(
'a.logout-link.item',
{
onclick: () => rs.logout(),
style: { marginTop: 'auto', cursor: 'pointer' },
},
[m('i.fas.fa-sign-out-alt'), m('span', 'Logout')]
),
m(
'button.toggle-nav',
{
@ -157,3 +199,13 @@ m.route(document.getElementById('main'), '/', {
render: (v) => m(Layout, m(config, v.attrs)),
},
});
// v51 architectural fix: ensure event queue starts on direct route refresh
if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) {
rs.logon(
{ Authorization: `Basic ${btoa(`${rs.loginKey.username}:${rs.loginKey.passwd}`)}` },
() => { }, // displayAuthError
() => { }, // displayErrorMessage
() => { }
);
}

View File

@ -24,31 +24,28 @@ const SignedIdentiy = () => {
style: 'margin-top:160px;',
onclick: () => {
rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => {
console.log(owns.ids[0]);
console.log(v.attrs.name);
owns.ids.length > 0
? rs.rsJsonApiRequest(
'/rsIdentity/createIdentity',
{
id: owns.ids[0],
name: v.attrs.name,
pseudonimous: false,
pgpPassword: passphase,
},
(data) => {
const message = data.retval
? 'Successfully created identity.'
: 'An error occured while creating identity.';
console.log(message);
widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]);
}
)
'/rsIdentity/createIdentity',
{
id: owns.ids[0],
name: v.attrs.name,
pseudonimous: false,
pgpPassword: passphase,
},
(data) => {
const message = data.retval
? 'Successfully created identity.'
: 'An error occured while creating identity.';
widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]);
}
)
: widget.popupMessage([
m('h3', 'Create new Identity'),
m('hr'),
'An error occured while creating identity.',
]);
m('h3', 'Create new Identity'),
m('hr'),
'An error occured while creating identity.',
]);
});
},
},
@ -84,7 +81,6 @@ const CreateIdentity = () => {
style: 'border:1px solid black',
oninput: (e) => {
pseudonimous = e.target.value === 'true';
console.log(pseudonimous);
},
},
[
@ -99,10 +95,10 @@ const CreateIdentity = () => {
m(
'p',
'You can have one or more identities. ' +
'They are used when you chat in lobbies, ' +
'forums and channel comments. ' +
'They act as the destination for distant chat and ' +
'the Retroshare distant mail system.'
'They are used when you chat in lobbies, ' +
'forums and channel comments. ' +
'They act as the destination for distant chat and ' +
'the Retroshare distant mail system.'
),
m(
'button',
@ -111,18 +107,18 @@ const CreateIdentity = () => {
!pseudonimous
? widget.popupMessage(m(SignedIdentiy, { name }))
: rs.rsJsonApiRequest(
'/rsIdentity/createIdentity',
{
name,
pseudonimous,
},
(data) => {
const message = data.retval
? 'Successfully created identity.'
: 'An error occured while creating identity.';
widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]);
}
);
'/rsIdentity/createIdentity',
{
name,
pseudonimous,
},
(data) => {
const message = data.retval
? 'Successfully created identity.'
: 'An error occured while creating identity.';
widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]);
}
);
},
},
'Create'
@ -192,28 +188,28 @@ const EditIdentity = () => {
onclick: () => {
!peopleUtil.checksudo(v.attrs.details.mPgpId)
? widget.popupMessage([
m(SignedEditIdentity, {
name,
details: v.attrs.details,
}),
])
m(SignedEditIdentity, {
name,
details: v.attrs.details,
}),
])
: rs.rsJsonApiRequest(
'/rsIdentity/updateIdentity',
{
id: v.attrs.details.mId,
'/rsIdentity/updateIdentity',
{
id: v.attrs.details.mId,
name,
name,
// avatar: v.attrs.details.mAvatar.mData.base64,
pseudonimous: true,
},
(data) => {
const message = data.retval
? 'Successfully Updated identity.'
: 'An error occured while updating identity.';
widget.popupMessage([m('h3', 'Update Identity'), m('hr'), message]);
}
);
// avatar: v.attrs.details.mAvatar.mData.base64,
pseudonimous: true,
},
(data) => {
const message = data.retval
? 'Successfully Updated identity.'
: 'An error occured while updating identity.';
widget.popupMessage([m('h3', 'Update Identity'), m('hr'), message]);
}
);
},
},
'Save'
@ -277,10 +273,10 @@ const Identity = () => {
[
m('h4', details.mNickname),
details.mNickname &&
m(peopleUtil.UserAvatar, {
avatar: details.mAvatar,
firstLetter: details.mNickname.slice(0, 1).toUpperCase(),
}),
m(peopleUtil.UserAvatar, {
avatar: details.mAvatar,
firstLetter: details.mNickname.slice(0, 1).toUpperCase(),
}),
m('.details', [
m('p', 'ID:'),
m('p', details.mId),
@ -303,6 +299,16 @@ const Identity = () => {
: 'undefiend'
),
]),
m(
'button',
{
onclick: () =>
m.route.set('/chat/:userid/createdistantchat', {
userid: details.mId,
}),
},
'Chat'
),
m(
'button',
{

View File

@ -10,33 +10,28 @@ const UserAvatar = () => ({
const imageURI = v.attrs.avatar;
return imageURI === undefined || imageURI.mData.base64 === ''
? m(
'div.defaultAvatar',
{
// image isn't getting loaded
// ? m('img.defaultAvatar', {
// src: '../data/user.png'
// })
},
m('p', v.attrs.firstLetter)
)
'div.defaultAvatar',
{
// image isn't getting loaded
// ? m('img.defaultAvatar', {
// src: '../data/user.png'
// })
},
m('p', v.attrs.firstLetter)
)
: m('img.avatar', {
src: 'data:image/png;base64,' + imageURI.mData.base64,
});
src: 'data:image/png;base64,' + imageURI.mData.base64,
});
},
});
function contactlist(list) {
const result = [];
if (list !== undefined) {
list.map((id) => {
id.isSearched = true;
rs.rsJsonApiRequest('/rsIdentity/isARegularContact', { id: id.mGroupId }, (data) => {
if (data.retval) result.push(id);
console.log(data);
});
});
}
return result;
if (list === undefined) return [];
return list.filter((id) => {
id.isSearched = true;
const entry = rs.userList.userMap[id.mGroupId];
return entry && entry.isContact;
});
}
function sortUsers(list) {
@ -63,7 +58,7 @@ function sortIds(list) {
return list;
}
async function ownIds(consumer = () => {}, onlySigned = false) {
async function ownIds(consumer = () => { }, onlySigned = false) {
await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => {
if (onlySigned) {
consumer(sortIds(owns.ids));
@ -122,10 +117,10 @@ const regularcontactInfo = () => {
[
m('h4', details.mNickname),
details.mNickname &&
m(UserAvatar, {
avatar: details.mAvatar,
firstLetter: details.mNickname.slice(0, 1).toUpperCase(),
}),
m(UserAvatar, {
avatar: details.mAvatar,
firstLetter: details.mNickname.slice(0, 1).toUpperCase(),
}),
m('.details', [
m('p', 'ID:'),
m('p', details.mId),

View File

@ -68,10 +68,10 @@ const RsEventsType = {
const API_URL = 'http://127.0.0.1:9092';
const loginKey = {
username: '',
passwd: '',
isVerified: false,
url: API_URL,
username: sessionStorage.getItem('rs_username') || '',
passwd: sessionStorage.getItem('rs_passwd') || '',
isVerified: sessionStorage.getItem('rs_isVerified') === 'true',
url: sessionStorage.getItem('rs_url') || API_URL,
};
// Make this as object property?
@ -80,12 +80,30 @@ function setKeys(username, password, url = API_URL, verified = true) {
loginKey.passwd = password;
loginKey.url = url;
loginKey.isVerified = verified;
if (verified) {
sessionStorage.setItem('rs_username', username);
sessionStorage.setItem('rs_passwd', password);
sessionStorage.setItem('rs_url', url);
sessionStorage.setItem('rs_isVerified', 'true');
} else {
sessionStorage.removeItem('rs_isVerified');
}
}
function logout() {
setKeys('', '', loginKey.url, false);
m.route.set('/');
}
const connectionState = {
status: true,
};
function rsJsonApiRequest(
path,
data = {},
callback = () => {},
callback = () => { },
async = true,
headers = {},
handleDeserialize = JSON.parse,
@ -94,7 +112,9 @@ function rsJsonApiRequest(
) {
headers['Accept'] = 'application/json';
if (loginKey.isVerified) {
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
if (loginKey.username && loginKey.passwd) {
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
}
}
// NOTE: After upgrading to mithrilv2, options.extract is no longer required
// since the status will become part of return value and then
@ -117,23 +137,42 @@ function rsJsonApiRequest(
headers,
body: data,
config,
xhr: config,
})
.then((result) => {
if (result.status === 200) {
callback(result.body, true);
connectionState.status = true;
try {
callback(result.body, true);
} catch (e) {
console.error('[RS] Error in success callback for path:', path, e);
}
} else {
if(result.status === 403 || result.status === 401)
loginKey.isVerified = false;
callback(result, false);
m.route.set('/');
connectionState.status = false;
if (result.status === 401 || result.status === 403) {
setKeys(loginKey.username, loginKey.passwd, loginKey.url, false);
m.route.set('/');
} else if (result.status === 0) {
console.error('[RS] Retroshare-jsonapi not available.');
} else {
console.error('[RS] HTTP error:', result.status, result.statusText);
}
try {
callback(result, false);
} catch (e) {
console.error('[RS] Error in error callback for path:', path, e);
}
}
return result;
})
.catch(function (e) {
callback(e, false);
console.error('Error: While sending request for path:', path, '\ninfo:', e);
m.route.set('/');
connectionState.status = false;
try {
callback(e, false);
} catch (cbErr) {
// console.error('[RS] Error in catch callback for path:', path, cbErr);
}
console.error('[RS] Error: While sending request for path:', path, '\ninfo:', e);
});
}
@ -176,10 +215,10 @@ const eventQueue = {
// #define RS_CHAT_TYPE_PUBLIC 1
// #define RS_CHAT_TYPE_PRIVATE 2
2: (chatId) => chatId.distant_chat_id, // distant chat (initiate? -> todo accept)
// #define RS_CHAT_TYPE_LOBBY 3
3: (chatId) => chatId.lobby_id.xstr64, // lobby_id
// #define RS_CHAT_TYPE_DISTANT 4
1: (cid) => hexId(cid),
2: (cid) => hexId(cid),
3: (cid) => hexId(cid),
4: (cid) => hexId(cid),
},
messages: {},
chatMessages: (chatId, owner, action) => {
@ -195,25 +234,36 @@ const eventQueue = {
)
)
) {
console.info('unknown chat event', chatId);
if (chatId) {
// Silent match
}
}
},
handler: (event, owner) =>
owner.chatMessages(event.mChatMessage.chat_id, owner, (r) => {
console.info('adding chat', r, event.mChatMessage);
r.push(event.mChatMessage);
owner.notify(event.mChatMessage);
}),
notify: () => {},
handler: (event, owner) => {
if (event && event.mChatMessage && event.mChatMessage.chat_id) {
owner.chatMessages(event.mChatMessage.chat_id, owner, (r) => {
r.push(event.mChatMessage);
owner.notify(event.mChatMessage);
});
} else if (event && event.mCid) {
// Administrative chat event (e.g. lobby info change, peer join/leave)
// Silent for now to avoid console spam, as actual messages use mChatMessage
}
},
notify: () => { },
},
[RsEventsType.GXS_CIRCLES]: {
// Circles (ignore in the meantime)
handler: (event, owner) => {},
handler: (event, owner) => { },
},
[RsEventsType.SHARED_DIRECTORIES]: {
// Deprecated/Administrative (ignore quietly)
handler: (event, owner) => { },
},
},
handler: (event) => {
if (!deeperIfExist(eventQueue.events, event.mType, (owner) => owner.handler(event, owner))) {
console.info('unhandled event', event);
// Ignore unhandled events silently
}
},
};
@ -221,20 +271,71 @@ const eventQueue = {
const userList = {
users: [],
userMap: {},
pendingIds: new Set(),
fetchTimer: null,
triggerFetch: () => {
if (userList.fetchTimer) return;
userList.fetchTimer = setTimeout(() => {
userList.fetchTimer = null;
if (userList.pendingIds.size === 0) return;
const ids = Array.from(userList.pendingIds);
userList.pendingIds.clear();
userList.fetchBulk(ids);
}, 1000);
},
fetchBulk: (ids) => {
// Chunk requests to avoid too large payloads if necessary, but for now 100 is safe
const chunkSize = 100;
for (let i = 0; i < ids.length; i += chunkSize) {
const chunk = ids.slice(i, i + chunkSize);
rsJsonApiRequest('/rsIdentity/getIdentitiesInfo', { ids: chunk }, (data, success) => {
if (success && data.idsInfo) {
data.idsInfo.forEach((info) => {
const gid = info.mMeta && info.mMeta.mGroupId;
if (gid) {
userList.userMap[gid] = {
name: info.mMeta.mGroupName,
isContact: info.mIsAContact,
};
}
});
m.redraw();
}
});
}
},
loadUsers: () => {
rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (list) => {
if (list !== undefined) {
console.info('loading ' + list.ids.length + ' users ...');
if (list !== undefined && list.ids) {
userList.users = list.ids;
userList.userMap = list.ids.reduce((a, c) => {
a[c.mGroupId] = c.mGroupName;
a[c.mGroupId] = { name: c.mGroupName, isContact: false };
return a;
}, {});
// Fetch contact status and details in bulk immediately
userList.fetchBulk(list.ids.map((u) => u.mGroupId));
}
});
},
username: (id) => {
return userList.userMap[id] || id;
if (!id) return '';
const entry = userList.userMap[id];
const name = typeof entry === 'object' ? entry.name : entry;
if (!name && id.length > 10) {
if (!userList.pendingIds.has(id)) {
userList.pendingIds.add(id);
userList.triggerFetch();
}
return id;
}
return name || id;
},
};
@ -251,70 +352,96 @@ const userList = {
function startEventQueue(
info,
loginHeader = {},
displayAuthError = () => {},
displayErrorMessage = () => {},
successful = () => {}
displayAuthError = () => { },
displayErrorMessage = () => { },
successful = () => { }
) {
return rsJsonApiRequest(
'/rsEvents/registerEventsHandler',
{},
(data, success) => {
if (success) {
// unused
} else if (data.status === 401) {
const xhr = new window.XMLHttpRequest();
let lastIndex = 0;
xhr.open('POST', loginKey.url + '/rsEvents/registerEventsHandler', true);
// Set headers for authentication
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
...loginHeader,
};
if (loginKey.isVerified && !headers['Authorization']) {
if (loginKey.username && loginKey.passwd) {
headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
}
}
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 401) {
displayAuthError('Incorrect login/password.');
} else if (data.status === 0) {
displayErrorMessage([
'Retroshare-jsonapi not available.',
m('br'),
'Please fix host and/or port.',
]);
} else {
displayErrorMessage('Login failed: HTTP ' + data.status + ' ' + data.statusText);
}
},
true,
loginHeader,
JSON.parse,
JSON.stringify,
(xhr, args, url) => {
let lastIndex = 0;
xhr.onprogress = (ev) => {
const currIndex = xhr.responseText.length;
if (currIndex > lastIndex) {
const parts = xhr.responseText.substring(lastIndex, currIndex);
lastIndex = currIndex;
parts
.trim()
.split('\n\n')
.filter((e) => e.startsWith('data: {'))
.map((e) => e.substr(6))
.map(JSON.parse)
.forEach((data) => {
}
};
xhr.onprogress = (ev) => {
const currIndex = xhr.responseText.length;
if (currIndex > lastIndex) {
const parts = xhr.responseText.substring(lastIndex, currIndex);
lastIndex = currIndex;
parts
.trim()
.split('\n\n')
.filter((e) => e.trim().length > 0)
.forEach((e) => {
if (e.startsWith('data: {')) {
try {
const data = JSON.parse(e.substr(6));
if (Object.prototype.hasOwnProperty.call(data, 'retval')) {
console.info(
info + ' [' + data.retval.errorCategory + '] ' + data.retval.errorMessage
);
data.retval.errorNumber === 0
? successful()
: displayErrorMessage(
`${info} failed: [${data.retval.errorCategory}] ${data.retval.errorMessage}`
);
if (data.retval.errorNumber !== 0) {
displayErrorMessage(
`${info} failed: [${data.retval.errorCategory}] ${data.retval.errorMessage}`
);
} else {
successful();
}
} else if (Object.prototype.hasOwnProperty.call(data, 'event')) {
data.event.queueSize = currIndex;
eventQueue.handler(data.event);
try {
eventQueue.handler(data.event);
} catch (err) {
console.error('[RS] Error in event handler:', err, data.event);
}
}
});
if (currIndex > 1e5) {
// max 100 kB eventQueue
startEventQueue('restart queue');
xhr.abort();
} catch (err) {
console.error('[RS] JSON parse error for part:', e, err);
}
}
}
};
return xhr;
});
if (currIndex > 1e6) {
// max 1 MB eventQueue
startEventQueue('restart queue');
xhr.abort();
}
}
);
};
xhr.onload = () => { };
xhr.onerror = (err) => {
console.error('[RS] Event Queue XHR error occurred:', err);
// Retry after 5 seconds to avoid silent event loss
setTimeout(() => {
console.log('[RS] Retrying event queue connection...');
startEventQueue(info, loginHeader, displayAuthError, displayErrorMessage, successful);
}, 5000);
};
// We need to send an eventType to registerEventsHandler
// 0 means all events
xhr.send(JSON.stringify({ eventType: 0 }));
return xhr;
}
function logon(loginHeader, displayAuthError, displayErrorMessage, successful) {
@ -333,8 +460,32 @@ function formatBytes(bytes, decimals = 2) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function hexId(id) {
if (!id) return '';
if (typeof id === 'string') return id;
if (typeof id === 'number') return String(id);
if (typeof id === 'object') {
// 1. Check for xstr64 (64-bit wrapped ID)
if (id.xstr64 && id.xstr64 !== '0') return id.xstr64;
// 2. Search for any hex string of appropriate length (128-bit or 64-bit)
const keys = Object.keys(id);
for (let i = 0; i < keys.length; i++) {
const val = id[keys[i]];
if (typeof val === 'string' && val.length >= 16 && val !== '00000000000000000000000000000000') return val;
// Search deeper for nested xstr64
if (val && typeof val === 'object' && val.xstr64 && val.xstr64 !== '0') return val.xstr64;
}
// 3. Last resort fallbacks
if (id.xstr64 !== undefined) return String(id.xstr64);
}
return String(id);
}
module.exports = {
rsJsonApiRequest,
idToHex: hexId,
connectionState,
setKeys,
setBackgroundTask,
logon,
@ -343,4 +494,5 @@ module.exports = {
userList,
loginKey,
formatBytes,
logout,
};

View File

@ -1,6 +1,7 @@
// -----------------------------------------------------------------------------
// This file contains all application-wide Sass mixins.
// -----------------------------------------------------------------------------
@use 'sass:color';
@use './colors' as *;
/// Button Mixin
@ -38,13 +39,17 @@
position: relative;
line-height: 1.2;
@if ($type == 'info') {
color: transparentize($dark-color, 0.2);
border: 1px solid transparentize($primary-retro-color, 0.2);
} @else if ($type == 'warning') {
border: 1px solid transparentize($golden-yellow-color, 0.2);
} @else if ($type == 'danger') {
border: 1px solid transparentize($red-color, 0.2);
@if ($type =='info') {
color: color.adjust($dark-color, $alpha: -0.2);
border: 1px solid color.adjust($primary-retro-color, $alpha: -0.2);
}
@else if ($type =='warning') {
border: 1px solid color.adjust($golden-yellow-color, $alpha: -0.2);
}
@else if ($type =='danger') {
border: 1px solid color.adjust($red-color, $alpha: -0.2);
}
&::before {
@ -53,13 +58,17 @@
top: 0.5rem;
left: 0.5rem;
@if ($type == 'info') {
@if ($type =='info') {
content: '\f05a';
color: $primary-color;
} @else if ($type == 'warning') {
}
@else if ($type =='warning') {
content: '\f071';
color: $golden-yellow-color;
} @else if ($type == 'danger') {
}
@else if ($type =='danger') {
content: '\f05a';
color: $red-color;
}
@ -69,15 +78,15 @@
@mixin flex($direction: '', $justify: '', $align: '', $gap: 0) {
display: flex;
@if ($direction != '') {
@if ($direction !='') {
flex-direction: $direction;
}
@if ($justify != '') {
@if ($justify !='') {
justify-content: $justify;
}
@if ($align != '') {
@if ($align !='') {
align-items: $align;
}
@ -88,8 +97,7 @@
// Font
@mixin fontdef-woff($FontPath, $FontName, $FontVersion: '1.0.0', $FontType: 'Regular') {
src:
url('#{$FontPath}/#{$FontName}-#{$FontType}.woff2') format('woff2'),
url('#{$FontPath}/#{$FontName}-#{$FontType}.woff') format('woff'),
url('#{$FontPath}/#{$FontName}-#{$FontType}.ttf') format('truetype');
}
src: url('#{$FontPath}/#{$FontName}-#{$FontType}.woff2') format('woff2'),
url('#{$FontPath}/#{$FontName}-#{$FontType}.woff') format('woff'),
url('#{$FontPath}/#{$FontName}-#{$FontType}.ttf') format('truetype');
}

View File

@ -1,20 +1,24 @@
@use 'sass:color';
@use '../abstracts' as *;
.media-item {
display: flex;
margin-top: 0.5rem;
padding: 1rem;
border: 1px solid transparentize($dark-color, 0.9);
border: 1px solid color.adjust($dark-color, $alpha: -0.9);
border-radius: 4px;
&__details {
flex-basis: 40%;
@include flex($align: start, $gap: 0.5rem);
& img {
width: 6rem;
object-fit: contain;
}
}
&__desc {
flex-basis: 60%;
}
}
}

View File

@ -1,5 +1,10 @@
@use 'sass:color';
@use '../abstracts' as *;
.active-link {
background: rgba(255, 255, 255, 0.1) !important;
}
/* Navbar */
.nav-menu {
background-color: $dark-color;
@ -41,7 +46,7 @@
transition: 0ms;
&:hover {
background-color: transparentize($light-color, 0.85);
background-color: color.adjust($light-color, $alpha: -0.85);
}
i.sidenav-icon {
@ -54,7 +59,7 @@
.item.item-selected {
color: $primary-light-color;
background-color: transparentize($primary-light-color, 0.85);
background-color: color.adjust($primary-light-color, $alpha: -0.85);
font-weight: medium;
}
@ -64,7 +69,7 @@
padding: 0;
top: 0;
right: -1rem;
background: lighten($primary-color, 15%);
background: color.adjust($primary-color, $lightness: 15%);
width: 1.5rem;
height: 1.5rem;
aspect-ratio: 1;
@ -77,8 +82,16 @@
&.collapsed {
.nav-menu__logo {
& h5 {
display: none;
.logo-container {
@include flex(column, $align: center, $gap: 0.5rem);
&>*:not(img) {
display: block;
}
}
& .nav-menu__logo-text {
display: none !important;
}
}
@ -89,8 +102,9 @@
justify-content: center;
transition: 300ms;
& span,
& p {
display: none;
display: none !important;
}
}
}
@ -132,7 +146,7 @@
}
.sidebarquickview {
& > h6 {
&>h6 {
padding: 0.5rem;
}
@ -172,3 +186,43 @@
border-left: 5px solid #3ba4d7;
}
}
/* GLOBAL SIDEBAR TO TABS */
@media (max-width: 700px) {
.tab-content {
flex-direction: column;
}
.sidebar {
width: 100% !important;
flex-direction: row !important;
overflow-x: auto !important;
overflow-y: hidden !important;
white-space: nowrap !important;
border-bottom: 1px solid rgba(20, 20, 27, 0.1) !important;
background: white !important;
z-index: 50 !important;
flex-shrink: 0 !important;
height: auto !important;
padding: 0 !important;
}
.sidebar a {
display: inline-block !important;
padding: 0.8rem 1.2rem !important;
border-bottom: 3px solid transparent !important;
border-left: none !important;
}
.sidebar .selected-sidebar-link {
border-left: none !important;
border-bottom: 3px solid #3ba4d7 !important;
animation: none !important;
}
/* Hide quickview headings on mobile to save space if needed */
.sidebarquickview>h4,
.sidebarquickview>h6 {
display: none !important;
}
}

View File

@ -1,3 +1,4 @@
@use 'sass:color';
@use '../abstracts' as *;
.posts {
@ -5,29 +6,34 @@
margin-top: 1rem;
flex-direction: column;
overflow: auto;
&__heading {
@include flex(column, $justify: space-between);
}
&-container {
height: 100%;
padding: 1rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 2rem;
border: 1px solid transparentize($dark-color, 0.9);
border: 1px solid color.adjust($dark-color, $alpha: -0.9);
border-radius: 4px;
overflow: auto;
&-card {
min-height: 240px;
flex-direction: column;
border: 1px solid transparentize($dark-color, 0.5);
border: 1px solid color.adjust($dark-color, $alpha: -0.5);
border-radius: 4px;
cursor: pointer;
text-align: center;
& img {
flex-basis: 90%;
object-fit: cover;
}
& p {
padding: 0 0.125rem;
flex-basis: 10%;
@ -37,4 +43,4 @@
}
}
}
}
}

View File

@ -1,3 +1,4 @@
@use 'sass:color';
@use '../abstracts/colors' as *;
.progress-bar {
@ -8,6 +9,7 @@
background-color: $light-color;
border-radius: 20px;
overflow: hidden;
&__status {
position: absolute;
top: 0;
@ -16,6 +18,7 @@
color: $dark-color;
background-color: $primary-color;
}
&__percent {
position: absolute;
inset: 0;
@ -23,6 +26,7 @@
width: fit-content;
height: fit-content;
}
&-chunks {
position: relative;
margin-top: 0.5rem;
@ -32,21 +36,27 @@
border-radius: 0.25rem;
overflow: hidden;
background-color: $light-color;
& .chunk {
width: 100%;
&[data-chunkVal='0'] {
background-color: transparentize($primary-light-color, 0.8);
background-color: color.adjust($primary-light-color, $alpha: -0.8);
}
&[data-chunkVal='1'] {
background-color: $red-color;
}
&[data-chunkVal='2'] {
background-color: $primary-color;
}
&[data-chunkVal='3'] {
background-color: $golden-yellow-color;
}
}
&__percent {
position: absolute;
inset: 0;
@ -55,4 +65,4 @@
height: fit-content;
}
}
}
}

View File

@ -1,3 +1,5 @@
@use '../abstracts' as *;
.lobby {
margin: 10px;
border: 1px solid #aaa;
@ -14,7 +16,7 @@
color: #666;
}
.lobby > .topic {
.lobby>.topic {
font-size: 0.95em;
margin-left: 25px;
margin-bottom: 5px;
@ -35,7 +37,7 @@
font-size: 1em;
}
.leftlobby > .topic {
.leftlobby>.topic {
font-size: 0.75em;
margin-left: 15px;
margin-bottom: 5px;
@ -106,7 +108,7 @@
margin-right: 5px;
}
.message > * {
.message>* {
margin-left: 5px;
}
@ -151,7 +153,7 @@ textarea.chatMsg {
font-size: 1.2em;
}
.setup > .identity {
.setup>.identity {
cursor: pointer;
}
@ -162,3 +164,183 @@ textarea.chatMsg {
.createDistantChat {
margin-top: 1em;
}
.no-lobbies {
.messages,
.chatMessage,
.setup {
left: 165px;
}
}
/* CHAT ROOM (Single Chat) - Desktop Grid Layout */
@media (min-width: 900px) {
.node-panel.chat-room {
display: grid !important;
grid-template-columns: 250px 1fr 200px !important;
/* Lobbies, Chat, Users */
grid-template-rows: auto 1fr auto !important;
/* Header, Messages, Input */
grid-template-areas:
"lobbies header rightbar"
"lobbies messages rightbar"
"lobbies input rightbar" !important;
padding: 0 !important;
height: 100% !important;
}
.node-panel.chat-room .lobbyName {
grid-area: header;
padding: 10px;
border-bottom: 1px solid #eee;
margin: 0;
z-index: 10;
background: white;
}
.node-panel.chat-room .lobbies {
grid-area: lobbies;
position: static !important;
width: auto !important;
height: auto !important;
border-right: 1px solid #ccc;
overflow-y: auto;
display: block !important;
top: auto !important;
bottom: auto !important;
left: auto !important;
}
.node-panel.chat-room .messages {
grid-area: messages;
position: static !important;
width: auto !important;
height: auto !important;
overflow-y: auto;
padding: 10px;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
margin: 0 !important;
}
.node-panel.chat-room .rightbar {
grid-area: rightbar;
position: static !important;
width: auto !important;
border-left: 1px solid #ccc;
overflow-y: auto;
display: block !important;
}
.node-panel.chat-room .chatMessage {
grid-area: input;
position: static !important;
width: auto !important;
height: auto !important;
border-top: 1px solid #eee;
left: auto !important;
right: auto !important;
bottom: auto !important;
flex: 0 0 auto;
padding: 10px !important;
background: white;
z-index: 10;
}
}
/* Mobile Overrides - Ensure Flex Column */
@media (max-width: 899px) {
.node-panel.chat-room {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
position: relative !important;
}
.node-panel.chat-room .lobbyName {
flex: 0 0 auto;
}
.node-panel.chat-room .messages {
flex: 1 !important;
overflow-y: auto !important;
position: relative !important;
top: 0 !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
height: auto !important;
margin: 0 !important;
}
.node-panel.chat-room .chatMessage {
flex: 0 0 auto !important;
position: relative !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
height: auto !important;
z-index: 100;
}
.node-panel.chat-room .rightbar,
.node-panel.chat-room .lobbies {
display: none !important;
position: fixed !important;
top: 60px !important;
bottom: 0 !important;
width: 80% !important;
background: white !important;
z-index: 200 !important;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2) !important;
}
.node-panel.chat-room.show-lobbies .lobbies {
display: block !important;
left: 0 !important;
}
.node-panel.chat-room.show-users .rightbar {
display: block !important;
right: 0 !important;
}
.chat-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 150;
}
.show-lobbies .chat-overlay,
.show-users .chat-overlay {
display: block;
}
/* Mobile Icons in Header */
.mobile-menu-icons {
display: flex;
gap: 15px;
font-size: 1.2rem;
}
.mobile-menu-icons i {
cursor: pointer;
padding: 5px;
}
}
@media (min-width: 900px) {
.mobile-menu-icons {
display: none;
}
}

View File

@ -279,3 +279,117 @@ table.friendsfiles td:nth-child(2) {
}
@include flex($justify: space-between);
}
/* FILES MODULE RESPONSIVENESS */
@media (max-width: 700px) {
/* General Files Layout */
.file-view__body-details {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
/* File Detail Grid (Downloads/Uploads) */
.file-view__body-details-stat {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.file-view__body-details-stat span {
display: flex;
align-items: center;
}
/* Share Manager Table -> Cards */
.share-manager__table,
.share-manager__table thead,
.share-manager__table tbody,
.share-manager__table tr,
.share-manager__table td {
display: block;
width: 100% !important;
}
.share-manager__table thead {
display: none;
/* Hide header on mobile */
}
.share-manager__table tr {
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 1rem;
padding: 0.5rem;
background: white;
}
.share-manager__table td {
margin-bottom: 0.5rem;
border: none !important;
padding-left: 0 !important;
}
/* My Files / Friends Files Tables -> Cards */
table.myfiles,
table.myfiles tr,
table.myfiles td,
table.friendsfiles,
table.friendsfiles tr,
table.friendsfiles td {
display: block;
width: 100% !important;
}
table.myfiles th,
table.friendsfiles th {
display: none;
}
table.myfiles tr,
table.friendsfiles tr {
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 1rem;
padding: 0.5rem;
background: white;
}
/* Search Container Layout */
.file-search-container {
flex-direction: column;
}
.file-search-container__keywords {
flex-basis: auto;
width: 100%;
border-right: none;
border-bottom: 1px solid rgba(20, 20, 27, 0.1);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
/* Search results table -> cards */
.results-container,
.results-container thead,
.results-container tbody,
.results-container tr,
.results-container td {
display: block;
width: 100% !important;
}
.results-container thead {
display: none;
}
.results-container tr {
border-bottom: 1px solid #eee;
padding: 1rem 0;
}
.results-container td {
margin-bottom: 0.5rem;
word-break: break-all;
}
}

View File

@ -1,3 +1,4 @@
@use 'sass:color';
@use '../abstracts' as *;
.homepage {
@ -6,20 +7,25 @@
.logo {
@include flex($justify: center, $align: center);
& img {
width: 90px;
}
.retroshareText {
@include flex(column, $align: center);
& .retrotext {
font-size: 36px;
font-weight: 600;
line-height: 1.125;
& > span {
&>span {
color: $primary-retro-color;
}
}
& > b {
&>b {
font-size: 14px;
line-height: 1;
}
@ -28,30 +34,36 @@
.certificate {
@include flex(column, $gap: 4rem);
&__heading {
text-align: center;
& > h1 {
&>h1 {
margin-bottom: 1rem;
}
}
&__content {
@include flex(column, $gap: 2rem);
padding: 2rem;
text-align: center;
border: 1.5px solid transparentize($primary-retro-color, 0.8);
border: 1.5px solid color.adjust($primary-retro-color, $alpha: -0.8);
border-radius: 6px;
box-shadow: 0px 0px 8px 2px transparentize($dark-color, 0.95);
.rsId > p {
box-shadow: 0px 0px 8px 2px color.adjust($dark-color, $alpha: -0.95);
.rsId>p {
margin-bottom: 0.5rem;
color: $primary-retro-color;
}
.retroshareID {
padding: 0.25rem;
@include flex($align: center);
justify-self: start;
font-size: 1.25rem;
border-radius: 4px;
background: transparentize($dark-color, 0.95);
background: color.adjust($dark-color, $alpha: -0.95);
& .textArea {
padding: 0;
width: 100%;
@ -62,10 +74,12 @@
border: none;
resize: none;
}
& i {
color: $primary-retro-color;
}
& > i {
&>i {
margin: 0 0.5rem;
cursor: pointer;
}
@ -76,27 +90,31 @@
background: whitesmoke;
@include flex($justify: center, $align: center, $gap: 0.5rem);
border-radius: 4px;
border: 1px solid transparentize($dark-color, 0.5);
border: 1px solid color.adjust($dark-color, $alpha: -0.5);
width: fit-content;
cursor: pointer;
&-container {
display: grid;
place-items: center;
}
&:hover {
background: $light-color;
border: 1px solid $dark-color;
}
& > i {
&>i {
font-size: 1.2rem;
color: green;
}
}
.add-friend > h6,
.webhelp-container > h6 {
.add-friend>h6,
.webhelp-container>h6 {
font-weight: normal;
margin-bottom: 0.5rem;
}
}
}
}
}

View File

@ -1,16 +1,16 @@
@use 'sass:color';
@use '../abstracts' as *;
.login-page {
background-image: linear-gradient(
-45deg,
transparentize($primary-color, 0.25),
transparentize($primary-retro-color, 0.25)
);
background-image: linear-gradient(-45deg,
color.adjust($primary-color, $alpha: -0.25),
color.adjust($primary-retro-color, $alpha: -0.25));
height: 100%;
animation: fadein 0.5s;
.login-container {
background-color: white;
box-shadow: 3px 3px 5px transparentize($dark-color, 0.6);
box-shadow: 3px 3px 5px color.adjust($dark-color, $alpha: -0.6);
margin: auto;
position: relative;
top: 100px;
@ -27,21 +27,24 @@
& * {
margin-bottom: 1rem;
}
& > img {
&>img {
margin: 1rem 0 2rem;
}
& extra {
margin: 0;
}
& > a {
&>a {
text-decoration: underline;
cursor: pointer;
}
}
.extra > label,
.extra > br,
.extra > input {
.extra>label,
.extra>br,
.extra>input {
margin-bottom: 0;
}
}
}

View File

@ -1,132 +1,240 @@
lockfileVersion: '6.0'
lockfileVersion: '9.0'
devDependencies:
sass:
specifier: ^1.60.0
version: 1.60.0
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
sass:
specifier: ^1.60.0
version: 1.97.3
packages:
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: true
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: true
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
'@parcel/watcher-darwin-arm64@2.5.6':
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: true
'@parcel/watcher-darwin-x64@2.5.6':
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
/immutable@4.3.0:
resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}
dev: true
'@parcel/watcher-freebsd-x64@2.5.6':
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
'@parcel/watcher-linux-arm-glibc@2.5.6':
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.6':
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.6':
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.6':
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
dev: true
/is-extglob@2.1.1:
immutable@5.1.5:
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: true
/is-glob@4.0.3:
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
sass@1.97.3:
resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==}
engines: {node: '>=14.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
snapshots:
'@parcel/watcher-android-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-x64@2.5.6':
optional: true
'@parcel/watcher-freebsd-x64@2.5.6':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.6':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.6':
optional: true
'@parcel/watcher-win32-arm64@2.5.6':
optional: true
'@parcel/watcher-win32-ia32@2.5.6':
optional: true
'@parcel/watcher-win32-x64@2.5.6':
optional: true
'@parcel/watcher@2.5.6':
dependencies:
detect-libc: 2.1.2
is-glob: 4.0.3
node-addon-api: 7.1.1
picomatch: 4.0.3
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.6
'@parcel/watcher-darwin-arm64': 2.5.6
'@parcel/watcher-darwin-x64': 2.5.6
'@parcel/watcher-freebsd-x64': 2.5.6
'@parcel/watcher-linux-arm-glibc': 2.5.6
'@parcel/watcher-linux-arm-musl': 2.5.6
'@parcel/watcher-linux-arm64-glibc': 2.5.6
'@parcel/watcher-linux-arm64-musl': 2.5.6
'@parcel/watcher-linux-x64-glibc': 2.5.6
'@parcel/watcher-linux-x64-musl': 2.5.6
'@parcel/watcher-win32-arm64': 2.5.6
'@parcel/watcher-win32-ia32': 2.5.6
'@parcel/watcher-win32-x64': 2.5.6
optional: true
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
detect-libc@2.1.2:
optional: true
immutable@5.1.5: {}
is-extglob@2.1.1:
optional: true
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
dev: true
optional: true
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: true
node-addon-api@7.1.1:
optional: true
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: true
picomatch@4.0.3:
optional: true
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
readdirp@4.1.2: {}
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sass@1.97.3:
dependencies:
picomatch: 2.3.1
dev: true
chokidar: 4.0.3
immutable: 5.1.5
source-map-js: 1.2.1
optionalDependencies:
'@parcel/watcher': 2.5.6
/sass@1.60.0:
resolution: {integrity: sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==}
engines: {node: '>=12.0.0'}
hasBin: true
dependencies:
chokidar: 3.5.3
immutable: 4.3.0
source-map-js: 1.0.2
dev: true
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: true
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: true
source-map-js@1.2.1: {}

File diff suppressed because one or more lines are too long

View File

@ -72,7 +72,7 @@ win32-g++ {
} else {
# create dummy files, we need it to include files on first try
system(webui-src/make-src/build.sh .)
system(bash webui-src/make-src/build.sh .)
WEBUI_SRC_SCRIPT = webui-src/make-src/build.sh
@ -80,19 +80,19 @@ win32-g++ {
create_webfiles_html.output = webui/index.html
create_webfiles_html.input = WEBUI_SRC_HTML
create_webfiles_html.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ index.html .
create_webfiles_html.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ index.html .
create_webfiles_html.variable_out = JUNK
create_webfiles_html.CONFIG = combine no_link
create_webfiles_js.output = webui/app.js
create_webfiles_js.input = WEBUI_SRC_JS
create_webfiles_js.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ app.js .
create_webfiles_js.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ app.js .
create_webfiles_js.variable_out = JUNK
create_webfiles_js.CONFIG = combine no_link
create_webfiles_css.output = webui/styles.css
create_webfiles_css.input = WEBUI_SRC_CSS
create_webfiles_css.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ styles.css .
create_webfiles_css.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ styles.css .
create_webfiles_css.variable_out = JUNK
create_webfiles_css.CONFIG = combine no_link