//------------------------------------------------ユーズ表示系関数-------------------------------------------------- var global_userid; var account_id; function view_ueuse_init(user_id, loginid) { global_userid = user_id; global_account_id = loginid; return true; } const mentionCache = {}; const fetchingMentions = false; async function replaceMentions(text) { const placeholders = []; let index = 0; // aタグの一時置き換え text = text.replace(/]*>.*?<\/a>/gi, (match) => { const placeholder = `\u2063{{PLACEHOLDER${index}}}\u2063`; placeholders.push(match); index++; return placeholder; }); const mentionMatches = [...text.matchAll(/@([a-zA-Z0-9_]+)/g)]; if (mentionMatches.length === 0) { placeholders.forEach((original, i) => { text = text.replace(`\u2063{{PLACEHOLDER${i}}}\u2063`, original); }); return text; } // ユーザーIDを小文字に正規化 const uniqueMentions = [...new Set(mentionMatches.map(match => match[1]))]; const mentionsToFetch = uniqueMentions.filter(userID => !mentionCache[userID]); if (mentionsToFetch.length > 0) { await new Promise((resolve) => { $.ajax({ url: '../function/get_userid.php', method: 'POST', data: { get_account: mentionsToFetch.join(','), userid: global_userid, account_id: global_account_id }, dataType: 'json', timeout: 300000, success: function (response) { if (response.success && response.users) { for (const [name, userInfo] of Object.entries(response.users)) { if (userInfo && userInfo.userid && userInfo.username) { mentionCache[name] = `@${userInfo.username}`; } else { mentionCache[name] = `@${name}`; } } } resolve(); }, error: function () { for (const name of mentionsToFetch) { mentionCache[name] = `@${name}`; } resolve(); } }); }); } // 元のtextに適用(小文字で照合) text = text.replace(/@([a-zA-Z0-9_]+)/g, (_, id) => { const lower = id; return mentionCache[lower] || `@${id}`; // 表示は元の大文字小文字を保持 }); // aタグ戻す placeholders.forEach((original, i) => { text = text.replace(`\u2063{{PLACEHOLDER${i}}}\u2063`, original); }); return text; } const CACHE_KEY = 'emojiCache'; const CACHE_EXPIRY_KEY = 'emojiCacheExpiry'; const CACHE_LIFETIME_MS = 24 * 60 * 60 * 1000; // 24時間 let emojiCache = {}; let fetchingEmojis = {}; (function loadEmojiCache() { const savedCache = localStorage.getItem(CACHE_KEY); const savedExpiry = localStorage.getItem(CACHE_EXPIRY_KEY); if (savedCache && savedExpiry && Date.now() < Number(savedExpiry)) { try { emojiCache = JSON.parse(savedCache); } catch (e) { localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_EXPIRY_KEY); } } else { localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_EXPIRY_KEY); } })(); function saveEmojiCache() { localStorage.setItem(CACHE_KEY, JSON.stringify(emojiCache)); localStorage.setItem(CACHE_EXPIRY_KEY, (Date.now() + CACHE_LIFETIME_MS).toString()); } async function replaceCustomEmojis(text) { const emojiMatches = [...text.matchAll(/:([a-zA-Z0-9_]+):/g)]; if (emojiMatches.length === 0) return text; const uniqueEmojis = [...new Set(emojiMatches.map(match => match[1]))]; const emojisToFetch = uniqueEmojis.filter(name => !emojiCache[name] && !fetchingEmojis[name]); if (emojisToFetch.length > 0) { const fetchPromise = new Promise((resolve) => { $.ajax({ url: '../function/get_customemoji.php', method: 'POST', data: { emoji: emojisToFetch.join(','), userid: global_userid, account_id: global_account_id }, dataType: 'json', timeout: 30000, success: function (response) { if (response.success && response.emojis) { for (const name of emojisToFetch) { if (response.success && response.emojis) { for (const name of emojisToFetch) { if (response.emojis[name]) { const emoji = response.emojis[name]; emojiCache[name] = emoji.emojipath; } else { emojiCache[name] = null; } } } } } else { for (const name of emojisToFetch) { emojiCache[name] = null; } } saveEmojiCache(); resolve(); }, error: function () { for (const name of emojisToFetch) { emojiCache[name] = null; } saveEmojiCache(); resolve(); } }); }); emojisToFetch.forEach(name => { fetchingEmojis[name] = fetchPromise; }); await fetchPromise; } await Promise.all(uniqueEmojis.map(name => fetchingEmojis[name])); text = text.replace(/:([a-zA-Z0-9_]+):/g, (_, name) => { const url = emojiCache[name]; if (url === undefined) return `:${name}:`; // 未取得 if (url === null) return `:${name}:`; // 存在しない return `:${name}:`; // ここで生成 }); return text; } function a_link(text) { const placeholders = {}; let placeholderIndex = 0; text = text.replace(/'/g, (match) => { const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`; placeholders[key] = match; // 元の文字列を保存 return key; }); text = text.replace(/(https:\/\/[\w!?\/+\-_~;.,*&@#$%()+|https:\/\/[ぁ-んァ-ヶ一ー-龠々\w\-\/?=&%.]+)/g, function (url) { const escapedUrl = url; const no_https_link = escapedUrl.replace("https://", ""); if (no_https_link.length > 48) { const truncatedLink = no_https_link.substring(0, 48) + '...'; return `${truncatedLink}`; } else { return `${no_https_link}`; } }); text = text.replace(/(^|[^a-zA-Z0-9_])#([a-zA-Z0-9ぁ-んァ-ン一-龥ー_]+)/gu, function (match, before, tag) { const encodedTag = encodeURIComponent("#" + tag); return `${before}#${tag}`; }); for (const key in placeholders) { const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedKey, 'g'), placeholders[key]); } return text; } function formatMarkdown(text) { const placeholders = {}; let placeholderIndex = 0; // URLをプレースホルダーに退避 text = text.replace(/]*>[\s\S]*?<\/a>/g, (match) => { const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`; placeholders[key] = match; // 元の文字列を保存 return key; }); // 複数行インラインコード(バッククォート3つ)を検出して、
で囲む
    text = text.replace(/```([\s\S]+?)```/g, (match, code) => {
        const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`;
        placeholders[key] = `
${code.replace(/^\s*\n/, '')}
`; return key; }); // コードブロックの退避 text = text.replace(/`([^`\n]+)`/g, (_, code) => { const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`; placeholders[key] = `${code}`; return key; }); // コロンで囲まれた絵文字をプレースホルダーに退避 text = text.replace(/:([a-zA-Z0-9_]+):/g, (match) => { const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`; placeholders[key] = match; // 元の文字列を保存 return key; }); // ユーザーIDをプレースホルダーに退避 text = text.replace(/@([a-zA-Z0-9_]+)/g, (match) => { const key = `\u2063{{PLACEHOLDER${placeholderIndex++}}}\u2063`; placeholders[key] = match; // 元の文字列を保存 return key; }); // 独自構文などの装飾 text = text.replace(/\[\[buruburu (.+?)\]\]/g, '$1'); text = text.replace(/\[\[time (\d+)\]\]/g, (_, ts) => { const d = new Date(parseInt(ts, 10) * 1000); return `${d.toLocaleString()}`; }); // マークダウン風装飾 text = text .replace(/\*\*\*(.+?)\*\*\*/g, '$1') .replace(/___(.+?)___/g, '$1') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/__(.+?)__/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/_(.+?)_/g, '$1') .replace(/~~(.+?)~~/g, '$1') .replace(/^>>> ?(.*)$/gm, '$1') // ここを修正 .replace(/\|\|(.+?)\|\|/g, '$1') .replace(/^# (.+)/gm, '

$1

') .replace(/^## (.+)/gm, '

$1

') .replace(/^### (.+)/gm, '

$1

') .replace(/^- (.+)/gm, '・ $1'); // 行ごとに

タグで囲む const lines = text.split('\n').map(line => { line = line.trim(); return line === '' ? '
' : `

${line}

`; }); // プレースホルダーを戻す let final = lines.join(''); for (const key in placeholders) { const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); final = final.replace(new RegExp(escapedKey, 'g'), placeholders[key]); } return final; } function YouTube_and_nicovideo_Links(postText) { const urlPattern = /(https:\/\/[^\s<>\[\]'"“”]+)/g; const urls = postText.match(urlPattern); let embedCode = ''; if (!urls) return null; let embeddedOnce = false; // ← 埋め込みが1回されたかどうか urls.forEach(url => { if (embeddedOnce) return; // ← すでに埋め込みしたらスキップ try { const parsed = new URL(url); const host = parsed.hostname.replace(/^www\./, ''); let videoId = ''; let videoTime = '0'; let iframe = false; if (['youtube.com', 'youtu.be', 'm.youtube.com'].includes(host)) { if (parsed.hostname === 'youtu.be') { videoId = parsed.pathname.replace('/', ''); iframe = true; } else if (parsed.searchParams.has('v')) { videoId = parsed.searchParams.get('v'); iframe = true; } else if (parsed.pathname.startsWith('/shorts/')) { videoId = parsed.pathname.replace('/shorts/', ''); iframe = true; } if (parsed.searchParams.has('t') || parsed.searchParams.has('start')) { videoTime = parsed.searchParams.get('t') || parsed.searchParams.get('start') || '0'; if (isNaN(parseInt(videoTime))) videoTime = '0'; } if (iframe && videoId) { embedCode = `
`; embeddedOnce = true; } } else if (['nicovideo.jp', 'nico.ms'].includes(host)) { if (parsed.pathname.includes('/watch/')) { videoId = parsed.pathname.split('/watch/')[1]; iframe = true; } else { videoId = parsed.pathname.replace('/', ''); iframe = true; } if (parsed.searchParams.has('from')) { videoTime = parsed.searchParams.get('from'); if (isNaN(parseInt(videoTime))) videoTime = '0'; } if (iframe && videoId) { embedCode = `
`; embeddedOnce = true; } } else { embedCode = null } } catch (e) { // 無視 } }); return embedCode; } function formatSmartDate(datetimeStr) { const date = new Date(datetimeStr.replace(" ", "T")); const now = new Date(); const diffMs = now - date; const diffAbs = Math.abs(diffMs); const future = diffMs < 0; const pad = (n) => n.toString().padStart(2, '0'); const hhmm = `${pad(date.getHours())}:${pad(date.getMinutes())}`; const y = date.getFullYear(); const m = date.getMonth(); const d = date.getDate(); const nowY = now.getFullYear(); const nowM = now.getMonth(); const nowD = now.getDate(); const dayDiff = Math.floor((new Date(y, m, d) - new Date(nowY, nowM, nowD)) / (1000 * 60 * 60 * 24)); if (!future && diffAbs < 30 * 1000) return "今"; if (future && diffAbs < 60 * 1000) return "まもなく"; if (future && diffAbs < 60 * 60 * 1000) return `${Math.floor(diffAbs / 1000 / 60)}分後`; if (dayDiff === 0) return `今日 ${hhmm}`; if (dayDiff === 1) return `明日 ${hhmm}`; if (!future && y === nowY && m === 0 && d === 1) return `元日 ${hhmm}`; if (y === nowY) return `${pad(m + 1)}/${pad(d)} ${hhmm}`; return `${y}/${pad(m + 1)}/${pad(d)} ${hhmm}`; } function getCheckIcon(userdata) { if (userdata["role"] && userdata["role"].includes("official")) { return `
`; } return ""; } function getBotIcon(userdata) { if (userdata["is_bot"] && userdata["is_bot"] == true) { return `
Bot
`; } return ""; } async function createUeuseHtml(ueuse, selectedUniqid = null) { let html = ""; let check = ""; let bot = ""; var reuse = ""; let contentHtml = ""; var uniqid = ""; var userid = ""; var username = ""; var iconurl = ""; var datetime = ""; var favoritecount = 0; var replycount = 0; var reusecount = 0; var is_favorite = false; var is_bookmark = false; var is_nsfw = false; var abi = ""; var abi_date = ""; var abi_html = ""; var addabi = ""; var inyo = ""; var img1 = ""; var img2 = ""; var img3 = ""; var img4 = ""; var vid1 = ""; var img_html = ""; var vid_html = ""; var nsfw_html = ""; var nsfw_start_html = ""; var nsfw_end_html = ""; if (ueuse["type"] == "Reuse") { if (ueuse["reuse"]) { check = getCheckIcon(ueuse["reuse"]["userdata"]); bot = getBotIcon(ueuse["reuse"]["userdata"]); } if (ueuse["ueuse"].length > 0) { reuse = ``; if (!(ueuse["reuse"] == null)) { // カスタム絵文字を非同期に差し替え var inyoreuseHtml = formatMarkdown(a_link(ueuse["reuse"]["ueuse"])); inyoreuseHtml = await replaceMentions(inyoreuseHtml); inyoreuseHtml = await replaceCustomEmojis(inyoreuseHtml); inyo = ``; } else { inyo = `

リユーズ元のユーズは削除されました。

`; } contentHtml = formatMarkdown(a_link(ueuse["ueuse"])); uniqid = ueuse["uniqid"]; userid = ueuse["userdata"]["userid"]; username = ueuse["userdata"]["username"]; iconurl = ueuse["userdata"]["iconurl"]; datetime = ueuse["datetime"]; favoritecount = ueuse["favoritecount"]; replycount = ueuse["replycount"]; reusecount = ueuse["reusecount"]; is_favorite = ueuse["is_favorite"]; is_bookmark = ueuse["is_bookmark"]; is_nsfw = ueuse["nsfw"]; img1 = ueuse["photo1"]; img2 = ueuse["photo2"]; img3 = ueuse["photo3"]; img4 = ueuse["photo4"]; vid1 = ueuse["video1"]; abi = ueuse["abi"]["abi_text"]; abi_date = ueuse["abi"]["abi_date"]; } else { if (!(ueuse["reuse"] == null)) { reuse = ``; inyo = ``; contentHtml = formatMarkdown(a_link(ueuse["reuse"]["ueuse"])); uniqid = ueuse["reuse"]["uniqid"]; userid = ueuse["reuse"]["userdata"]["userid"]; username = ueuse["reuse"]["userdata"]["username"]; iconurl = ueuse["reuse"]["userdata"]["iconurl"]; datetime = ueuse["reuse"]["datetime"]; favoritecount = ueuse["reuse"]["favoritecount"]; replycount = ueuse["reuse"]["replycount"]; reusecount = ueuse["reuse"]["reusecount"]; is_favorite = ueuse["reuse"]["is_favorite"]; is_bookmark = ueuse["reuse"]["is_bookmark"]; is_nsfw = ueuse["reuse"]["nsfw"]; img1 = ueuse["reuse"]["photo1"]; img2 = ueuse["reuse"]["photo2"]; img3 = ueuse["reuse"]["photo3"]; img4 = ueuse["reuse"]["photo4"]; vid1 = ueuse["reuse"]["video1"]; abi = ueuse["reuse"]["abi"]["abi_text"]; abi_date = ueuse["reuse"]["abi"]["abi_date"]; } else { reuse = ``; inyo = ``; contentHtml = "リユーズ元のユーズは削除されました。"; uniqid = ueuse["uniqid"]; userid = ueuse["userdata"]["userid"]; username = ueuse["userdata"]["username"]; iconurl = ueuse["userdata"]["iconurl"]; datetime = ueuse["datetime"]; favoritecount = ueuse["favoritecount"]; replycount = ueuse["replycount"]; reusecount = ueuse["reusecount"]; is_favorite = ueuse["is_favorite"]; is_bookmark = ueuse["is_bookmark"]; is_nsfw = ueuse["nsfw"]; img1 = ueuse["photo1"]; img2 = ueuse["photo2"]; img3 = ueuse["photo3"]; img4 = ueuse["photo4"]; vid1 = ueuse["video1"]; abi = ueuse["abi"]["abi_text"]; abi_date = ueuse["abi"]["abi_date"]; } } } else if (ueuse["type"] == "Reply") { check = getCheckIcon(ueuse["userdata"]); bot = getBotIcon(ueuse["userdata"]); if (selectedUniqid != null && selectedUniqid == ueuse["uniqid"]) { reuse = `

一番上のユーズに返信

`; } else { reuse = `

一番上のユーズに返信

`; } inyo = ``; contentHtml = formatMarkdown(a_link(ueuse["ueuse"])); uniqid = ueuse["uniqid"]; userid = ueuse["userdata"]["userid"]; username = ueuse["userdata"]["username"]; iconurl = ueuse["userdata"]["iconurl"]; datetime = ueuse["datetime"]; favoritecount = ueuse["favoritecount"]; replycount = ueuse["replycount"]; reusecount = ueuse["reusecount"]; is_favorite = ueuse["is_favorite"]; is_bookmark = ueuse["is_bookmark"]; is_nsfw = ueuse["nsfw"]; img1 = ueuse["photo1"]; img2 = ueuse["photo2"]; img3 = ueuse["photo3"]; img4 = ueuse["photo4"]; vid1 = ueuse["video1"]; abi = ueuse["abi"]["abi_text"]; abi_date = ueuse["abi"]["abi_date"]; } else if (ueuse["type"] == "User") { html = ` `; return html; } else { check = getCheckIcon(ueuse["userdata"]); bot = getBotIcon(ueuse["userdata"]); reuse = ``; inyo = ``; contentHtml = formatMarkdown(a_link(ueuse["ueuse"])); uniqid = ueuse["uniqid"]; userid = ueuse["userdata"]["userid"]; username = ueuse["userdata"]["username"]; iconurl = ueuse["userdata"]["iconurl"]; datetime = ueuse["datetime"]; favoritecount = ueuse["favoritecount"]; replycount = ueuse["replycount"]; reusecount = ueuse["reusecount"]; is_favorite = ueuse["is_favorite"]; is_bookmark = ueuse["is_bookmark"]; is_nsfw = ueuse["nsfw"]; img1 = ueuse["photo1"]; img2 = ueuse["photo2"]; img3 = ueuse["photo3"]; img4 = ueuse["photo4"]; vid1 = ueuse["video1"]; abi = ueuse["abi"]["abi_text"]; abi_date = ueuse["abi"]["abi_date"]; } if (abi != "" && typeof abi === "string") { abi = formatMarkdown(a_link(abi)); abi = await replaceMentions(abi); abi = await replaceCustomEmojis(abi); abi_html = `

`+ await replaceCustomEmojis(username) + `さんが追記しました

`+ abi + `

`+ formatSmartDate(abi_date) + `
`; addabi = ``; } else { abi_html = ``; if (global_userid == userid) { addabi = ``; } else { addabi = ``; } } let is_fav = { "class": "favbtn", "icon": "../img/sysimage/favorite_1.svg#favorite" }; if (is_favorite === true) { is_fav = { "class": "favbtn favbtn_after", "icon": "../img/sysimage/favorite_2.svg#favorite" }; } let is_reu = { "class": "reuse" }; if (ueuse["type"] == "Reuse") { if (!(ueuse["ueuse"].length > 0)) { if (global_userid == ueuse["userdata"]["userid"]) { is_reu = { "class": "reuse reuse_after" }; } } } let is_bok = { "class": "bookmark", "icon": "../img/sysimage/bookmark_1.svg#bookmark_1" }; if (is_bookmark === true) { is_bok = { "class": "bookmark bookmark_after", "icon": "../img/sysimage/bookmark_1.svg#bookmark_1" }; } if (is_nsfw == true) { nsfw_html = `

NSFW指定がされている投稿です!
職場や公共の場での表示には適さない場合があります。
表示ボタンを押すと表示されます。

` nsfw_start_html = `
` nsfw_end_html = `
` } if (img1.length > 0) { if (img2.length > 0) { if (img3.length > 0) { if (img4.length > 0) { img_html = ``; } else { img_html = ``; } } else { img_html = ``; } } else { img_html = ``; } } else { img_html = ``; } if (vid1.length > 0) { vid_html = `
`; } // カスタム絵文字を非同期に差し替え contentHtml = await replaceMentions(contentHtml); contentHtml = await replaceCustomEmojis(contentHtml); if (ueuse["type"] == "Reuse") { if (ueuse["ueuse"].length > 0) { if (YouTube_and_nicovideo_Links(ueuse["ueuse"])) { contentHtml = contentHtml + YouTube_and_nicovideo_Links(ueuse["ueuse"]); } } else { if (YouTube_and_nicovideo_Links(ueuse["reuse"]["ueuse"])) { contentHtml = contentHtml + YouTube_and_nicovideo_Links(ueuse["reuse"]["ueuse"]); } } } else { if (YouTube_and_nicovideo_Links(ueuse["ueuse"])) { contentHtml = contentHtml + YouTube_and_nicovideo_Links(ueuse["ueuse"]); } } var favbox = `
` + replycount + ` `+ addabi + `
` if (ueuse["is_activitypub"] == true) { favbox = ""; } html = `
`+ reuse + `
` + await replaceCustomEmojis(username) + `
`+ bot + ` `+ check + `
`+ formatSmartDate(datetime) + `
`+ nsfw_html + ` `+ nsfw_start_html + `
`+ contentHtml + `
`+ img_html + ` `+ vid_html + ` `+ inyo + ` `+ abi_html + ` `+ nsfw_end_html + ` `+ favbox + `
`; return html; } function createAdsHtml(ads) { if (!(ads == null || ads == "")) { var ads_html = ``; return ads_html; } else { var ads_html = ``; return ads_html; } } // 投稿一覧を非同期で全部HTML化 → そのあと順番通りにappend async function renderUeuses(ueuseData, selectedUniqid = null) { if (ueuseData["success"] == false) { var errmsg; if (ueuseData["error"] == "no_ueuse") { errmsg = "ユーズがありません"; } else if (ueuseData["error"] == "bad_request") { errmsg = "不正なリクエストが検出されました"; } $("#postContainer").append(`

` + errmsg + `

`); return true; } else { var htmlList = []; var ueuseList = ueuseData["ueuses"]; for (const ueuse of ueuseList) { const html = await createUeuseHtml(ueuse, selectedUniqid); htmlList.push(html); } var ads = ueuseData["ads"]; const ads_html = createAdsHtml(ads); htmlList.push(ads_html); // 投稿順を保ったままDOMへ追加 for (const html of htmlList) { $("#postContainer").append(html); } return true; } } async function createNotificationHtml(notification) { let html = ""; let is_readclass = ""; let datetime = notification["datetime"]; let userid = notification["userdata"]["userid"]; let username = notification["userdata"]["username"]; let iconurl = notification["userdata"]["iconurl"]; let title = notification["title"]; let content = formatMarkdown(a_link(notification["message"])); content = await replaceMentions(content); content = await replaceCustomEmojis(content); let url = notification["url"]; if(notification["is_read"] == false) { is_readclass = "this"; } html = `
`+formatSmartDate(datetime)+`

`+await replaceCustomEmojis(title)+`

`+content+`

詳細をみる
`; return html; } async function renderNotifications(notificationData) { if (notificationData["success"] == false) { var errmsg; if (notificationData["error"] == "no_notification") { errmsg = "通知がありません"; } else if (notificationData["error"] == "bad_request") { errmsg = "不正なリクエストが検出されました"; } $("#postContainer").append(`

` + errmsg + `

`); return true; } else { var htmlList = []; var notificationList = notificationData["notifications"]; for (const notification of notificationList) { const html = await createNotificationHtml(notification); htmlList.push(html); } // 投稿順を保ったままDOMへ追加 for (const html of htmlList) { $("#postContainer").append(html); } return true; } }