Imp: local profile avatar.

-Implemented the possibility to add a locally stored avatar quickly and easily during the voice chat, without resorting to editing the URL

-The avatar can be changed seamlessly and with no delay or reload

-It resets on reload

REASONS:
I implemented this feature because I felt the editing of the URL is a bit cluckly as a whole
This feature makes for a more streamlined experience for the end-user
This commit is contained in:
renatospirito17
2026-04-27 13:24:56 +02:00
parent e217556504
commit a17857fcd9
4 changed files with 142 additions and 2 deletions
+4 -2
View File
@@ -1616,17 +1616,19 @@ io.sockets.on('connect', async (socket) => {
let peer_id_to_update = null;
for (let peer_id in peers[room_id]) {
if (peers[room_id][peer_id]['peer_name'] == peer_name_old && peer_id == socket.id) {
if (peer_id == socket.id) {
peers[room_id][peer_id]['peer_name'] = peer_name_new;
peers[room_id][peer_id]['peer_avatar'] = peer_avatar;
// presenter
if (presenters && presenters[room_id] && presenters[room_id][peer_id]) {
presenters[room_id][peer_id]['peer_name'] = peer_name_new;
}
peer_id_to_update = peer_id;
log.debug('[' + socket.id + '] Peer name changed', {
log.debug('[' + socket.id + '] Peer profile changed', {
peer_name_old: peer_name_old,
peer_name_new: peer_name_new,
});
break;
}
}
+9
View File
@@ -3161,6 +3161,8 @@ button {
#activeRoomsBtn,
#myPeerNameSetBtn,
#myProfileAvatarUploadBtn,
#myProfileAvatarResetBtn,
#captionEveryoneBtn,
#muteEveryoneBtn,
#hideEveryoneBtn,
@@ -3177,6 +3179,8 @@ button {
}
#myPeerNameSetBtn:hover,
#myProfileAvatarUploadBtn:hover,
#myProfileAvatarResetBtn:hover,
#roomSendEmailBtn:hover,
#lockRoomBtn:hover,
#unlockRoomBtn:hover {
@@ -3220,6 +3224,11 @@ button {
transition: all 0.3s ease-in-out;
}
#myProfileAvatarUploadBtn,
#myProfileAvatarResetBtn {
margin-top: 6px;
}
/*--------------------------------------------------------------
# Settings Table
--------------------------------------------------------------*/
+120
View File
@@ -360,6 +360,9 @@ const tabLanguagesBtn = getId('tabLanguagesBtn');
const mySettingsCloseBtn = getId('mySettingsCloseBtn');
const myPeerNameSet = getId('myPeerNameSet');
const myPeerNameSetBtn = getId('myPeerNameSetBtn');
const myProfileAvatarFile = getId('myProfileAvatarFile');
const myProfileAvatarUploadBtn = getId('myProfileAvatarUploadBtn');
const myProfileAvatarResetBtn = getId('myProfileAvatarResetBtn');
const switchSounds = getId('switchSounds');
const switchShare = getId('switchShare');
const switchKeepButtonsVisible = getId('switchKeepButtonsVisible');
@@ -563,6 +566,8 @@ let thisMaxRoomParticipants = 8;
let swBg = 'rgba(0, 0, 0, 0.7)'; // swAlert background color
let isDocumentOnFullScreen = false;
let isToggleExtraBtnClicked = false;
const maxAvatarFileSizeBytes = 1 * 1024 * 1024; // 1MB in-memory avatar limit
let hasTemporaryAvatar = false;
// peer
let myPeerId; // This socket.id
@@ -847,6 +852,8 @@ function setButtonsToolTip() {
// Settings
setTippy(mySettingsCloseBtn, 'Close', 'bottom');
setTippy(myPeerNameSetBtn, 'Change name', 'top');
setTippy(myProfileAvatarUploadBtn, 'Upload temporary avatar', 'top');
setTippy(myProfileAvatarResetBtn, 'Reset temporary avatar', 'top');
setTippy(myRoomId, 'Room name (click to copy/share)', 'right');
setTippy(mySessionTime, 'Session time', 'right');
setTippy(
@@ -2695,6 +2702,17 @@ async function handleAddPeer(config) {
screenReaderAccessibility.announceMessage(`${peer_name} joined the room`);
}
/**
* Broadcast my current profile (name + avatar) to room peers
*/
function emitMyPeerProfile() {
sendToDataChannel({
type: 'peerAvatar',
peer_name: myPeerName,
peer_avatar: myPeerAvatar,
});
}
/**
* Handle peers connection state
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
@@ -2923,6 +2941,13 @@ async function handleRTCDataChannels(peer_id) {
case 'micVolume':
handlePeerVolume(dataMessage);
break;
case 'peerAvatar':
handlePeerName({
peer_id: peer_id,
peer_name: dataMessage.peer_name,
peer_avatar: dataMessage.peer_avatar,
});
break;
default:
break;
}
@@ -7121,6 +7146,16 @@ function setMySettingsBtn() {
myPeerNameSetBtn.addEventListener('click', (e) => {
updateMyPeerName();
});
myProfileAvatarUploadBtn.addEventListener('click', () => {
myProfileAvatarFile?.click();
});
myProfileAvatarFile.addEventListener('change', async (e) => {
await updateMyPeerAvatarInMemory(e);
});
myProfileAvatarResetBtn.addEventListener('click', () => {
resetMyPeerAvatarInMemory();
});
updateMyAvatarResetButtonVisibility();
// Sounds
switchSounds.addEventListener('change', (e) => {
notifyBySound = e.currentTarget.checked;
@@ -9651,6 +9686,11 @@ function createChatDataChannel(peer_id) {
chatDataChannels[peer_id] = peerConnections[peer_id].createDataChannel('mirotalk_chat_channel');
chatDataChannels[peer_id].onopen = (event) => {
console.log('chatDataChannels created', event);
if (hasTemporaryAvatar) {
chatDataChannels[peer_id].send(
JSON.stringify({ type: 'peerAvatar', peer_name: myPeerName, peer_avatar: myPeerAvatar }),
);
}
};
}
@@ -11634,6 +11674,8 @@ function isValidHttpURL(input) {
*/
function isImageURL(input) {
if (!input || typeof input !== 'string') return false;
// Allow in-memory avatars loaded from local file input.
if (input.startsWith('data:image/')) return true;
try {
const url = new URL(input);
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.svg'].some((ext) =>
@@ -12075,6 +12117,84 @@ async function updateMyPeerName() {
userLog('toast', 'My name changed to ' + myPeerName);
}
/**
* Update my avatar in-memory only (cleared on page refresh)
* @param {Event} event input file change event
*/
async function updateMyPeerAvatarInMemory(event) {
const file = event?.target?.files?.[0];
if (!file) return;
if (!file.type || !file.type.startsWith('image/')) {
myProfileAvatarFile.value = '';
return userLog('warning', 'Please select a valid image file');
}
if (file.size > maxAvatarFileSizeBytes) {
myProfileAvatarFile.value = '';
return userLog('warning', 'Avatar too large. Max allowed size is 1MB');
}
try {
const avatarDataUrl = await readFileAsDataUrl(file);
myPeerAvatar = avatarDataUrl;
hasTemporaryAvatar = true;
setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar);
setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar);
setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar);
updateMyAvatarResetButtonVisibility();
emitMyPeerProfile();
userLog('toast', 'Temporary avatar applied (will reset on refresh)');
} catch (err) {
console.error('Failed to read avatar file', err);
userLog('error', 'Unable to load avatar file');
} finally {
myProfileAvatarFile.value = '';
}
}
/**
* Reset in-memory avatar to default generated/fallback avatar
*/
function resetMyPeerAvatarInMemory() {
myPeerAvatar = false;
hasTemporaryAvatar = false;
setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar);
setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar);
setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar);
updateMyAvatarResetButtonVisibility();
emitMyPeerProfile();
userLog('toast', 'Temporary avatar reset');
}
/**
* Show reset avatar button only for uploaded temporary avatars
*/
function updateMyAvatarResetButtonVisibility() {
if (!myProfileAvatarResetBtn) return;
myProfileAvatarResetBtn.classList.toggle('hidden', !hasTemporaryAvatar);
if (myProfileAvatarUploadBtn) myProfileAvatarUploadBtn.classList.toggle('hidden', hasTemporaryAvatar);
}
/**
* Convert file to data URL
* @param {File} file
* @returns {Promise<string>}
*/
function readFileAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
/**
* Append updated peer name to video player
* @param {object} config data
+9
View File
@@ -1048,6 +1048,15 @@ access to use this app.
<div>
<img id="myProfileAvatar" />
</div>
<div>
<input id="myProfileAvatarFile" type="file" accept="image/*" class="hidden" />
<button id="myProfileAvatarUploadBtn">
<i class="fas fa-image"></i>&nbsp;Upload avatar
</button>
<button id="myProfileAvatarResetBtn" class="hidden">
<i class="fas fa-rotate-left"></i>&nbsp;Reset avatar
</button>
</div>
<br />
<div class="title">
<i class="fas fa-user"></i>