Skip to content

Commit 5bcb40d

Browse files
committed
v3.73.519
1 parent 76d4e5e commit 5bcb40d

10 files changed

Lines changed: 1322 additions & 9 deletions

File tree

css/no-inline.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,26 @@ html .nm-hidden { display: none; }
172172
.nm-tr-4 { width: 100%; box-sizing: border-box; margin: 0 0 12px; padding: 9px 12px; border-radius: var(--radius-sm); border: 1px solid var(--glass-border); background: rgba(255,255,255,0.05); color: var(--text); font-size: 0.9em; outline: none; }
173173
.nm-tr-5 { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; max-height: 320px; overflow-y: auto; padding-right: 4px; }
174174
.nm-tr-6 { padding: 10px 12px; border-radius: var(--radius-sm); border: 1px solid var(--glass-border); background: rgba(255,255,255,0.04); color: var(--text); cursor: pointer; font-size: 0.9em; transition: background 0.12s, border-color 0.12s; text-align: left; }
175+
.translate-lang-option.selected { background: rgba(255,255,255,0.12); border-color: var(--primary); color: var(--text-bright); }
176+
.translate-lang-option:disabled { opacity: 0.5; cursor: default; }
177+
178+
/* i18n.js — app-wide UI translation */
179+
.nym-i18n-bg-indicator { position: fixed; right: 16px; bottom: 16px; z-index: 10050; display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px; background: var(--glass-bg, rgba(20,20,20,0.85)); border: 1px solid var(--glass-border); color: var(--text-dim); font-size: 0.8em; box-shadow: 0 4px 16px rgba(0,0,0,0.3); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease; }
180+
.nym-i18n-bg-indicator.visible { opacity: 1; transform: translateY(0); }
181+
.nym-i18n-bg-spinner { width: 13px; height: 13px; border-radius: 50%; border: 2px solid var(--glass-border); border-top-color: var(--primary); animation: nym-i18n-spin 0.8s linear infinite; }
182+
@keyframes nym-i18n-spin { to { transform: rotate(360deg); } }
183+
.message-translation.auto { border-left-color: var(--secondary, var(--primary)); }
184+
.message-autotr-footer { width: 100%; margin-top: 3px; font-size: 0.8em; }
185+
.autotr-toggle { display: inline-flex; align-items: center; gap: 4px; background: none; border: none; padding: 2px; color: var(--text-dim); cursor: pointer; font-size: inherit; font-family: inherit; font-weight: 500; border-radius: var(--radius-xs); }
186+
.autotr-toggle:hover { color: var(--primary); }
187+
.autotr-icon { color: var(--primary); flex-shrink: 0; }
188+
.autotr-original { display: none; margin-top: 2px; padding: 6px 10px; background: rgba(255,255,255,0.04); border-left: 3px solid var(--text-dim); border-radius: 0 var(--radius-xs) var(--radius-xs) 0; color: var(--text-dim); line-height: 1.4; }
189+
.autotr-original.expanded { display: block; }
190+
.autotr-original-label { opacity: 0.7; font-size: 0.9em; }
191+
.auto-translate-scope { display: flex; flex-direction: column; gap: 8px; }
192+
.auto-translate-scope-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
193+
.auto-translate-scope-row > span { font-size: 0.9em; color: var(--text-dim); }
194+
.auto-translate-scope-row > select { max-width: 140px; }
175195

176196
/* zaps.js */
177197
.nm-zap-1 { display: inline-block; padding: 20px; border: 5px solid white; background: white; color: black; text-align: center; border-radius: 10px; }

functions/api/bot.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ async function publicCommandRateOk(request) {
151151
return true;
152152
}
153153
}
154-
var NYMCHAT_VERSION = "3.72.519";
154+
var NYMCHAT_VERSION = "3.73.519";
155155
var BOT_SATS_PER_CREDIT = 10;
156156
// The free public-channel Nymbot always uses this single best all-around model.
157157
// The premium private Nymbot routes each message to a task-specialised model.
@@ -2276,7 +2276,7 @@ var NYMBOT_SYSTEM_PROMPT = [
22762276
"Games & Fun: ?trivia [category] — AI-generated trivia (general, history, science, crypto, nostr), ?joke — AI-generated joke, ?riddle — AI-generated riddle, ?wordplay [mode] — AI word game (wordle, anagram, scramble), ?flip — Coin flip, ?8ball — Magic 8-ball, ?pick <options> — Random pick.",
22772277
"Utility: ?math <expr> — Calculate, ?units <value> <from> to <to> — Convert units, ?time — UTC time, ?btc — Current Bitcoin price.",
22782278
"Channel Activity: ?who — Active nyms in channel, ?summarize — AI summary of channel discussion, ?top — Top channels by activity, ?last [N] — Recent messages, ?seen <nym> — Where was someone last seen.",
2279-
"Info: ?help — List all bot commands, ?about — About Nymchat (version, platform links), ?nostr — Nostr protocol tips, ?changelog [version] — Live Nymchat release notes pulled from GitHub (default shows the latest release; pass a tag like ?changelog v3.72.519 for a specific version).",
2279+
"Info: ?help — List all bot commands, ?about — About Nymchat (version, platform links), ?nostr — Nostr protocol tips, ?changelog [version] — Live Nymchat release notes pulled from GitHub (default shows the latest release; pass a tag like ?changelog v3.73.519 for a specific version).",
22802280
"Users can also type @Nymbot <question> to ask me directly.",
22812281
"Users can quote-reply any message and mention @Nymbot to ask about it, or reply to my responses to continue the conversation with context.",
22822282
"",
@@ -3120,7 +3120,7 @@ function findRelease(releases, query) {
31203120
var t = (releases[i].tag || "").toLowerCase().replace(/^v/, "");
31213121
if (t === normalized) return releases[i];
31223122
}
3123-
// Prefix match (e.g. "3.61" matches "3.72.519")
3123+
// Prefix match (e.g. "3.61" matches "3.73.519")
31243124
for (var j = 0; j < releases.length; j++) {
31253125
var tt = (releases[j].tag || "").toLowerCase().replace(/^v/, "");
31263126
if (tt.indexOf(normalized) === 0) return releases[j];
@@ -3175,7 +3175,7 @@ function needsChangelogContext(question) {
31753175
if (/\b(changelog|release notes?|what'?s new|whats new|patch notes?|update notes?)\b/.test(q)) return true;
31763176
if (/\b(latest|newest|recent|new|previous|last)\b.{0,30}\b(release|version|update)\b/.test(q)) return true;
31773177
if (/\b(release|version|update)\b.{0,30}\b(history|notes?|log|info)\b/.test(q)) return true;
3178-
// Specific version reference like "3.72.519", "v3.61", "version 3.60.300"
3178+
// Specific version reference like "3.73.519", "v3.61", "version 3.60.300"
31793179
if (/\bv?\d+\.\d+(?:\.\d+)?\b/.test(q) && /\b(nym|nymchat|app|version|release|update)\b/.test(q)) return true;
31803180
return false;
31813181
}

index.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,14 @@ <h2>Report User/Content</h2>
13771377
<div class="form-hint">Auto matches your system preference</div>
13781378
</div>
13791379

1380+
<div class="form-group">
1381+
<label class="form-label">App Language</label>
1382+
<select class="form-select" id="uiLanguageSelect" data-no-i18n>
1383+
<option value="">English (default)</option>
1384+
</select>
1385+
<div class="form-hint">Translate the entire app interface into your language. Message text is handled separately by the translation settings below.</div>
1386+
</div>
1387+
13801388
<div class="form-group">
13811389
<label class="form-label">Theme</label>
13821390
<select class="form-select" id="themeSelect">
@@ -1751,6 +1759,42 @@ <h2>Report User/Content</h2>
17511759
<div class="form-hint">Choose your preferred language for translating messages via the context menu.</div>
17521760
</div>
17531761

1762+
<div class="form-group">
1763+
<label class="form-label">Auto-translate Messages</label>
1764+
<select class="form-select" id="autoTranslateSelect">
1765+
<option value="false">Disabled</option>
1766+
<option value="true">Enabled</option>
1767+
</select>
1768+
<div class="form-hint">Automatically translate messages in the conversation you are viewing that are not already in your translation language. Requires a translation language above.</div>
1769+
</div>
1770+
1771+
<div class="form-group" id="autoTranslateSubOptions" style="display:none;">
1772+
<label class="form-label">Auto-translate In</label>
1773+
<div class="auto-translate-scope">
1774+
<label class="auto-translate-scope-row">
1775+
<span>Channels</span>
1776+
<select class="form-select" id="autoTranslateChannelsSelect">
1777+
<option value="true">Enabled</option>
1778+
<option value="false">Disabled</option>
1779+
</select>
1780+
</label>
1781+
<label class="auto-translate-scope-row">
1782+
<span>Direct Messages</span>
1783+
<select class="form-select" id="autoTranslatePMsSelect">
1784+
<option value="true">Enabled</option>
1785+
<option value="false">Disabled</option>
1786+
</select>
1787+
</label>
1788+
<label class="auto-translate-scope-row">
1789+
<span>Groups</span>
1790+
<select class="form-select" id="autoTranslateGroupsSelect">
1791+
<option value="true">Enabled</option>
1792+
<option value="false">Disabled</option>
1793+
</select>
1794+
</label>
1795+
</div>
1796+
</div>
1797+
17541798
<div class="form-group">
17551799
<label class="form-label">Notification Sound</label>
17561800
<select class="form-select" id="soundSelect">
@@ -2249,6 +2293,7 @@ <h2>Report User/Content</h2>
22492293
<script defer src="js/modules/p2p.js"></script>
22502294
<script defer src="js/modules/calls.js"></script>
22512295
<script defer src="js/modules/translate.js"></script>
2296+
<script defer src="js/modules/i18n.js"></script>
22522297
<script defer src="js/modules/sidebar-sections.js"></script>
22532298
<script defer src="js/modules/columns.js"></script>
22542299
<script defer src="js/modules/polls.js"></script>

js/app.js

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@
293293
state.btnPrev.disabled = state.idx === 0;
294294
state.btnNext.textContent = (step.final || state.idx === state.steps.length - 1) ? 'Done' : 'Next';
295295

296+
// Swap in cached translations for this step immediately (no flash of
297+
// English) when the app UI is being translated.
298+
try {
299+
if (window.nym && typeof nym.i18nApplyNow === 'function') nym.i18nApplyNow(state.card);
300+
} catch (_) { }
301+
296302
positionStep();
297303
};
298304

@@ -363,6 +369,16 @@
363369

364370
buildSteps();
365371

372+
// If the UI is being translated, pre-translate the whole tutorial script
373+
// at high priority so each step is ready as the user advances.
374+
try {
375+
if (window.nym && typeof nym.i18nPrioritize === 'function' && Array.isArray(state.steps)) {
376+
const strings = [];
377+
state.steps.forEach(s => { if (s.title) strings.push(s.title); if (s.body) strings.push(s.body); });
378+
nym.i18nPrioritize(strings);
379+
}
380+
} catch (_) { }
381+
366382
state.overlay = document.getElementById('tutorialOverlay');
367383
state.card = document.getElementById('tutorialCard');
368384
state.highlight = document.getElementById('tutorialHighlight');
@@ -437,15 +453,39 @@
437453
state.started = false;
438454
}
439455

456+
// All tutorial step strings (title + body), so the i18n layer can
457+
// pre-translate the whole tour at high priority as soon as a language is
458+
// chosen — even though the tutorial is lazy-rendered later.
459+
window.nymTutorialStrings = function () {
460+
try {
461+
buildSteps();
462+
const out = [];
463+
(state.steps || []).forEach(s => { if (s.title) out.push(s.title); if (s.body) out.push(s.body); });
464+
return out;
465+
} catch (_) { return []; }
466+
};
467+
440468
// Expose helper to app
441469
window.maybeStartTutorial = function (force = false) {
442470
try {
443471
if (!force) {
444472
const seen = localStorage.getItem('nym_tutorial_seen') === 'true';
445473
if (seen) return;
446474
}
447-
// Delay a bit to let UI settle after login/restore
448-
setTimeout(() => startTutorial(), 300);
475+
const startNow = () => setTimeout(() => startTutorial(), 300);
476+
const nymRef = window.nym;
477+
const needPicker = !force && nymRef &&
478+
typeof nymRef.showUiLanguagePicker === 'function' &&
479+
typeof nymRef._uiLanguageChosen === 'function' && !nymRef._uiLanguageChosen();
480+
if (needPicker) {
481+
setTimeout(() => {
482+
nymRef.showUiLanguagePicker({ dismissible: true })
483+
.then(() => { nymRef._markUiLanguageChosen(); startNow(); })
484+
.catch(() => { nymRef._markUiLanguageChosen(); startNow(); });
485+
}, 400);
486+
return;
487+
}
488+
startNow();
449489
} catch (_) {
450490
setTimeout(() => startTutorial(), 300);
451491
}
@@ -3261,6 +3301,33 @@ async function showSettings() {
32613301
translateLangSelect.value = nym.settings.translateLanguage || '';
32623302
}
32633303

3304+
// App-wide UI language selector
3305+
if (typeof nym.populateUiLanguageSelect === 'function') nym.populateUiLanguageSelect();
3306+
const uiLangSelect = document.getElementById('uiLanguageSelect');
3307+
if (uiLangSelect) {
3308+
uiLangSelect.value = nym.getUiLanguage ? nym.getUiLanguage() : '';
3309+
uiLangSelect.onchange = function () {
3310+
const code = uiLangSelect.value || '';
3311+
if (typeof nym.applyUiLanguage === 'function') {
3312+
nym.applyUiLanguage(code).catch(() => { });
3313+
}
3314+
// Adopt the app language as the message translation language too.
3315+
if (typeof nym._syncTranslateLanguageToUi === 'function') nym._syncTranslateLanguageToUi(code);
3316+
if (typeof nym._markUiLanguageChosen === 'function') nym._markUiLanguageChosen();
3317+
if (typeof nostrSettingsSave === 'function') nostrSettingsSave();
3318+
};
3319+
}
3320+
3321+
// Auto-translate messages settings
3322+
if (typeof nym._syncAutoTranslateSettingsUI === 'function') nym._syncAutoTranslateSettingsUI();
3323+
const autoTrSelect = document.getElementById('autoTranslateSelect');
3324+
if (autoTrSelect) {
3325+
autoTrSelect.onchange = function () {
3326+
const sub = document.getElementById('autoTranslateSubOptions');
3327+
if (sub) sub.style.display = autoTrSelect.value === 'true' ? '' : 'none';
3328+
};
3329+
}
3330+
32643331
const gesturesEnabledSelect = document.getElementById('gesturesEnabledSelect');
32653332
const swipeLeftSelect = document.getElementById('swipeLeftActionSelect');
32663333
const swipeRightSelect = document.getElementById('swipeRightActionSelect');
@@ -3793,6 +3860,46 @@ async function saveSettings() {
37933860
localStorage.setItem('nym_translate_language', translateLangEl.value);
37943861
}
37953862

3863+
// Read and save app-wide UI language (already applied live via onchange,
3864+
// this just guarantees persistence if the select changed programmatically)
3865+
const uiLangEl = document.getElementById('uiLanguageSelect');
3866+
if (uiLangEl) {
3867+
const uiLang = uiLangEl.value || '';
3868+
if (uiLang !== (nym.getUiLanguage ? nym.getUiLanguage() : '')) {
3869+
if (typeof nym.applyUiLanguage === 'function') {
3870+
nym.applyUiLanguage(uiLang).catch(() => { });
3871+
}
3872+
if (typeof nym._syncTranslateLanguageToUi === 'function') nym._syncTranslateLanguageToUi(uiLang);
3873+
}
3874+
nym.settings.uiLanguage = uiLang;
3875+
}
3876+
3877+
// Read and save auto-translate settings
3878+
let autoTranslateChanged = false;
3879+
const autoTrEl = document.getElementById('autoTranslateSelect');
3880+
if (autoTrEl) {
3881+
const on = autoTrEl.value === 'true';
3882+
if (on !== !!nym.settings.autoTranslate) autoTranslateChanged = true;
3883+
nym.settings.autoTranslate = on;
3884+
localStorage.setItem('nym_auto_translate', String(on));
3885+
}
3886+
const autoTrScope = [
3887+
['autoTranslateChannelsSelect', 'autoTranslateChannels', 'nym_auto_translate_channels'],
3888+
['autoTranslatePMsSelect', 'autoTranslatePMs', 'nym_auto_translate_pms'],
3889+
['autoTranslateGroupsSelect', 'autoTranslateGroups', 'nym_auto_translate_groups'],
3890+
];
3891+
for (const [elId, key, lsKey] of autoTrScope) {
3892+
const el = document.getElementById(elId);
3893+
if (!el) continue;
3894+
const on = el.value === 'true';
3895+
if (on !== (nym.settings[key] !== false)) autoTranslateChanged = true;
3896+
nym.settings[key] = on;
3897+
localStorage.setItem(lsKey, String(on));
3898+
}
3899+
if (autoTranslateChanged && typeof nym.retranslateVisibleMessages === 'function') {
3900+
nym.retranslateVisibleMessages();
3901+
}
3902+
37963903
const VALID_SWIPE_ACTIONS = ['quote', 'translate', 'copy', 'react', 'zap', 'slap', 'hug', 'none'];
37973904
const gesturesEnabledEl = document.getElementById('gesturesEnabledSelect');
37983905
if (gesturesEnabledEl) {
@@ -4226,7 +4333,7 @@ function initWallpaperUI() {
42264333
}
42274334
}
42284335

4229-
const NYMCHAT_VERSION = 'v3.72.519';
4336+
const NYMCHAT_VERSION = 'v3.73.519';
42304337

42314338
const BUILD_REPO = 'https://github.com/Spl0itable/NYM';
42324339

@@ -6691,6 +6798,37 @@ async function applyNostrSettings(s) {
66916798
if (typeof nym._renderTranslateDropdownList === 'function') nym._renderTranslateDropdownList();
66926799
}
66936800

6801+
// App-wide UI language — adopt a language chosen on another device.
6802+
if (typeof s.uiLanguage === 'string' && s.uiLanguage !== (nym.getUiLanguage ? nym.getUiLanguage() : '')) {
6803+
if (typeof nym.applyUiLanguage === 'function') {
6804+
nym.applyUiLanguage(s.uiLanguage).catch(() => { });
6805+
} else {
6806+
nym.settings.uiLanguage = s.uiLanguage;
6807+
try { localStorage.setItem('nym_ui_language', s.uiLanguage); } catch (_) { }
6808+
}
6809+
if (typeof nym.populateUiLanguageSelect === 'function') nym.populateUiLanguageSelect();
6810+
}
6811+
6812+
// Auto-translate incoming messages (master + per-conversation-type gates)
6813+
if (typeof s.autoTranslate === 'boolean') {
6814+
nym.settings.autoTranslate = s.autoTranslate;
6815+
localStorage.setItem('nym_auto_translate', String(s.autoTranslate));
6816+
}
6817+
if (typeof s.autoTranslateChannels === 'boolean') {
6818+
nym.settings.autoTranslateChannels = s.autoTranslateChannels;
6819+
localStorage.setItem('nym_auto_translate_channels', String(s.autoTranslateChannels));
6820+
}
6821+
if (typeof s.autoTranslatePMs === 'boolean') {
6822+
nym.settings.autoTranslatePMs = s.autoTranslatePMs;
6823+
localStorage.setItem('nym_auto_translate_pms', String(s.autoTranslatePMs));
6824+
}
6825+
if (typeof s.autoTranslateGroups === 'boolean') {
6826+
nym.settings.autoTranslateGroups = s.autoTranslateGroups;
6827+
localStorage.setItem('nym_auto_translate_groups', String(s.autoTranslateGroups));
6828+
}
6829+
if (typeof nym._syncAutoTranslateSettingsUI === 'function') nym._syncAutoTranslateSettingsUI();
6830+
if (typeof nym.retranslateVisibleMessages === 'function') nym.retranslateVisibleMessages();
6831+
66946832
// Favorite custom emoji packs
66956833
if (Array.isArray(s.emojiPackFavorites)) {
66966834
nym._emojiPackFavorites = s.emojiPackFavorites.filter(k => typeof k === 'string');

0 commit comments

Comments
 (0)