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:
+4
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
--------------------------------------------------------------*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> Upload avatar
|
||||
</button>
|
||||
<button id="myProfileAvatarResetBtn" class="hidden">
|
||||
<i class="fas fa-rotate-left"></i> Reset avatar
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<div class="title">
|
||||
<i class="fas fa-user"></i>
|
||||
|
||||
Reference in New Issue
Block a user