mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat: Add clickable #channel links in chat messages
- Auto-detect #channel names in messages and convert to clickable links - Click existing channel: switch to it via channel selector - Click non-existing channel: join via API, then switch - Green styling to distinguish from blue @mentions - Only active in channel context (not in DMs) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -625,6 +625,45 @@ main {
|
||||
background-color: #084298;
|
||||
}
|
||||
|
||||
/* Channel Links (#channelname) */
|
||||
.channel-link {
|
||||
display: inline-block;
|
||||
background-color: #198754;
|
||||
color: white;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
margin: 0 0.1rem;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.channel-link:hover {
|
||||
background-color: #157347;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.message.own .channel-link {
|
||||
background-color: #0f5132;
|
||||
}
|
||||
|
||||
.message.own .channel-link:hover {
|
||||
background-color: #0d4429;
|
||||
}
|
||||
|
||||
.channel-link.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.channel-link.loading::after {
|
||||
content: '...';
|
||||
}
|
||||
|
||||
/* Quoted Text (»text«) */
|
||||
.quote-text {
|
||||
font-style: italic;
|
||||
|
||||
@@ -18,10 +18,13 @@ function processMessageContent(content) {
|
||||
// 1. Convert @[Username] mentions to badges
|
||||
processed = processMentions(processed);
|
||||
|
||||
// 2. Convert »quoted text« to styled quotes
|
||||
// 2. Convert #channel to clickable links (only in channel context)
|
||||
processed = processChannelLinks(processed);
|
||||
|
||||
// 3. Convert »quoted text« to styled quotes
|
||||
processed = processQuotes(processed);
|
||||
|
||||
// 3. Convert URLs to links (and images to thumbnails)
|
||||
// 4. Convert URLs to links (and images to thumbnails)
|
||||
processed = processUrls(processed);
|
||||
|
||||
return processed;
|
||||
@@ -43,6 +46,31 @@ function processMentions(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert #channelname to clickable channel links
|
||||
* Only active in channel context (when availableChannels exists)
|
||||
* @param {string} text - HTML-escaped text
|
||||
* @returns {string} - Text with channel links
|
||||
*/
|
||||
function processChannelLinks(text) {
|
||||
// Only process in channel context (app.js provides availableChannels)
|
||||
// In DM context (dm.js), availableChannels is undefined
|
||||
if (typeof availableChannels === 'undefined') {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Match #channelname pattern
|
||||
// Valid: alphanumeric, underscore, hyphen
|
||||
// Must be at least 2 characters after #
|
||||
// Must be preceded by whitespace, start of string, or punctuation
|
||||
const channelPattern = /(^|[\s.,!?:;()\[\]])#([a-zA-Z0-9_-]{2,})/g;
|
||||
|
||||
return text.replace(channelPattern, (_match, prefix, channelName) => {
|
||||
const escapedName = escapeHtmlAttribute(channelName);
|
||||
return `${prefix}<a href="#" class="channel-link" data-channel-name="${escapedName}">#${channelName}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert »quoted text« to styled quote blocks
|
||||
* @param {string} text - HTML-escaped text
|
||||
@@ -205,10 +233,107 @@ function initializeImageHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle channel link click - switch to or join channel
|
||||
* @param {string} channelName - Channel name without # prefix
|
||||
*/
|
||||
async function handleChannelLinkClick(channelName) {
|
||||
// Normalize name (add # if not present for comparison)
|
||||
const normalizedName = channelName.startsWith('#') ? channelName : '#' + channelName;
|
||||
|
||||
// Check if channel exists in availableChannels
|
||||
const existingChannel = availableChannels.find(
|
||||
ch => ch.name.toLowerCase() === normalizedName.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingChannel) {
|
||||
switchToChannel(existingChannel.index, existingChannel.name);
|
||||
} else {
|
||||
await joinAndSwitchToChannel(normalizedName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to an existing channel via the channel selector
|
||||
* @param {number} channelIdx - Channel index
|
||||
* @param {string} channelName - Channel name for notification
|
||||
*/
|
||||
function switchToChannel(channelIdx, channelName) {
|
||||
const selector = document.getElementById('channelSelector');
|
||||
if (selector) {
|
||||
selector.value = channelIdx;
|
||||
// Trigger change event to update state and load messages
|
||||
selector.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a channel via API when clicking channel link, then switch to it
|
||||
* @param {string} channelName - Channel name (with #)
|
||||
*/
|
||||
async function joinAndSwitchToChannel(channelName) {
|
||||
try {
|
||||
const response = await fetch('/api/channels/join', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name: channelName })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Joined channel "${channelName}"!`, 'success');
|
||||
|
||||
// Show warning if applicable (e.g., exceeding channel limit)
|
||||
if (data.warning) {
|
||||
setTimeout(() => {
|
||||
showNotification(data.warning, 'warning');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Reload channels and switch to new channel
|
||||
await loadChannels();
|
||||
switchToChannel(data.channel.index, channelName);
|
||||
} else {
|
||||
showNotification('Failed to join channel: ' + data.error, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error joining channel via link:', error);
|
||||
showNotification('Failed to join channel', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize channel link click handlers using event delegation
|
||||
*/
|
||||
function initializeChannelLinkHandlers() {
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('channel-link')) {
|
||||
e.preventDefault();
|
||||
|
||||
const channelName = e.target.getAttribute('data-channel-name');
|
||||
if (channelName) {
|
||||
// Add loading state
|
||||
e.target.classList.add('loading');
|
||||
|
||||
handleChannelLinkClick(channelName).finally(() => {
|
||||
e.target.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeImageHandlers);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeImageHandlers();
|
||||
initializeChannelLinkHandlers();
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded
|
||||
initializeImageHandlers();
|
||||
initializeChannelLinkHandlers();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user