feat(hamlog): the settings panel, the key check, and a row-colour channel
HAMLOG.online is now reachable: Settings → External services → HAMLOG.ONLINE takes the API key, the auto-upload switch and the timing, exactly like the six services beside it, and a link opens the page where the key is issued. The button next to it checks the KEY, not the connection. HAMLOG answers KEYSTATUS with the callsign the key belongs to — something no other service here offers — so the result reads "Key is valid — account F4BPO" rather than a green tick. A key pasted from a club account or a second station is visible at once, which is precisely the mistake a tick would have hidden. No on-close timing, for the same reason as Cloudlog: there is no per-QSO upload column to select a backlog from at shutdown, so the option would have done nothing. Immediate or delayed, and a stored on_close falls back to immediate rather than silently uploading nothing. The appearance rules gain a HAMLOG.online channel too, so a row can be coloured "confirmed" on that alone. It reads the ADIF extras — the standard names a field for hamlog.EU and none for hamlog.ONLINE — using the same keys the award engine reads, so the two can never disagree about what a confirmation is. "To send" skips it: a QSO is either uploaded or not, there is no card to be owed.
This commit is contained in:
@@ -29,9 +29,9 @@ const LABELS: Record<string, string> = {
|
||||
|
||||
// The channels a rule can be scoped to. "worked" is the catch-all — it means
|
||||
// nothing on ANY channel — so narrowing it would say nothing.
|
||||
const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz'] as const;
|
||||
const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz', 'hamlog'] as const;
|
||||
const CHANNEL_LABELS: Record<string, string> = {
|
||||
qsl: 'appr.chQsl', lotw: 'LoTW', eqsl: 'eQSL', qrz: 'QRZ.com',
|
||||
qsl: 'appr.chQsl', lotw: 'LoTW', eqsl: 'eQSL', qrz: 'QRZ.com', hamlog: 'HAMLOG.online',
|
||||
};
|
||||
|
||||
// MatrixColorsSection recolours the band/mode matrix — the PH/CW/DIG grid in the
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
GetPSUSettings, SavePSUSettings,
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
import type { main as mainModels, cluster as clusterModels } from '../../wailsjs/go/models';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { EventsOn, BrowserOpenURL } from '../../wailsjs/runtime/runtime';
|
||||
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
||||
@@ -1730,7 +1730,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// HRDLog only: publish the live frequency/mode/rig on hrdlog.net.
|
||||
on_air?: boolean;
|
||||
};
|
||||
type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg; hrdlog: ExtServiceCfg; eqsl: ExtServiceCfg; cloudlog: ExtServiceCfg; delete_remote?: boolean };
|
||||
type ExternalServices = { qrz: ExtServiceCfg; clublog: ExtServiceCfg; lotw: ExtServiceCfg; hrdlog: ExtServiceCfg; eqsl: ExtServiceCfg; cloudlog: ExtServiceCfg; hamlog: ExtServiceCfg; delete_remote?: boolean };
|
||||
const emptyExtCfg = (): ExtServiceCfg => ({
|
||||
api_key: '', url: '', station_id: '', email: '', username: '', password: '', callsign: '', code: '', qth_nickname: '',
|
||||
force_station_callsign: '', tqsl_path: '', station_location: '', key_password: '',
|
||||
@@ -1738,7 +1738,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
auto_upload: false, upload_mode: 'immediate', on_air: false,
|
||||
});
|
||||
const [extSvc, setExtSvc] = useState<ExternalServices>({
|
||||
qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(), hrdlog: emptyExtCfg(), eqsl: emptyExtCfg(), cloudlog: emptyExtCfg(), delete_remote: false,
|
||||
qrz: emptyExtCfg(), clublog: emptyExtCfg(), lotw: emptyExtCfg(), hrdlog: emptyExtCfg(), eqsl: emptyExtCfg(), cloudlog: emptyExtCfg(), hamlog: emptyExtCfg(), delete_remote: false,
|
||||
});
|
||||
const [qrzTest, setQrzTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [qrzTesting, setQrzTesting] = useState(false);
|
||||
@@ -1872,6 +1872,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [hrdlogTesting, setHrdlogTesting] = useState(false);
|
||||
const [cloudlogTest, setCloudlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [cloudlogTesting, setCloudlogTesting] = useState(false);
|
||||
const [hamlogTest, setHamlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [hamlogTesting, setHamlogTesting] = useState(false);
|
||||
const [eqslTest, setEqslTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [eqslTesting, setEqslTesting] = useState(false);
|
||||
const [stationLocations, setStationLocations] = useState<string[]>([]);
|
||||
@@ -1879,7 +1881,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// could not hold hooks at all; PanelHost lifted that restriction, and this
|
||||
// stays put because moving it down would reset the tab on every reopen —
|
||||
// a choice now, not a workaround.
|
||||
const [extSvcTab, setExtSvcTab] = useState<'qrz' | 'clublog' | 'hrdlog' | 'eqsl' | 'lotw' | 'cloudlog' | 'pota'>('qrz');
|
||||
const [extSvcTab, setExtSvcTab] = useState<'qrz' | 'clublog' | 'hrdlog' | 'eqsl' | 'lotw' | 'cloudlog' | 'hamlog' | 'pota'>('qrz');
|
||||
// POTA hunter-log sync (stamps pota_ref on local QSOs from your pota.app log).
|
||||
const [potaToken, setPotaToken] = useState('');
|
||||
const [potaBusy, setPotaBusy] = useState(false);
|
||||
@@ -5420,6 +5422,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{ k: 'eqsl', label: 'EQSL', ready: true },
|
||||
{ k: 'lotw', label: 'LOTW', ready: true },
|
||||
{ k: 'cloudlog', label: 'CLOUDLOG', ready: true },
|
||||
{ k: 'hamlog', label: 'HAMLOG.ONLINE', ready: true },
|
||||
{ k: 'pota', label: 'POTA', ready: true },
|
||||
];
|
||||
|
||||
@@ -5489,6 +5492,30 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}
|
||||
}
|
||||
|
||||
const hamlog = extSvc.hamlog ?? emptyExtCfg();
|
||||
const setHamlog = (patch: Partial<ExtServiceCfg>) =>
|
||||
setExtSvc((s) => ({ ...s, hamlog: { ...(s.hamlog ?? emptyExtCfg()), ...patch } }));
|
||||
|
||||
// Checking the KEY, not the connection.
|
||||
//
|
||||
// HAMLOG answers KEYSTATUS with the callsign the key belongs to, which no
|
||||
// other service here does — so the answer is shown as the callsign rather
|
||||
// than a bare "OK". A key pasted from a club account or a second station is
|
||||
// then visible immediately, which is exactly the mistake a green tick would
|
||||
// have hidden.
|
||||
async function testHamlog() {
|
||||
setHamlogTesting(true);
|
||||
setHamlogTest(null);
|
||||
try {
|
||||
const call = await CheckHamlogKey(hamlog.api_key ?? '');
|
||||
setHamlogTest({ ok: true, msg: call ? t('es.hamlogKeyOkCall', { call }) : t('es.hamlogKeyOk') });
|
||||
} catch (e: any) {
|
||||
setHamlogTest({ ok: false, msg: String(e?.message ?? e) });
|
||||
} finally {
|
||||
setHamlogTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const cloudlog = extSvc.cloudlog;
|
||||
const setCloudlog = (patch: Partial<ExtServiceCfg>) =>
|
||||
setExtSvc((s) => ({ ...s, cloudlog: { ...s.cloudlog, ...patch } }));
|
||||
@@ -5790,6 +5817,60 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : extSvcTab === 'hamlog' ? (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">{t('es.apiKey')}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={hamlog.api_key}
|
||||
onChange={(e) => setHamlog({ api_key: e.target.value })}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground -mt-1">
|
||||
{t('es.hamlogHint')}{' '}
|
||||
<button type="button" className="underline hover:text-foreground"
|
||||
onClick={() => BrowserOpenURL('https://hamlog.online/account/agent.php')}>
|
||||
hamlog.online/account/agent.php
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={hamlog.auto_upload}
|
||||
onCheckedChange={(c) => setHamlog({ auto_upload: !!c })}
|
||||
/>
|
||||
{t('es.autoUpload')}
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">{t('es.uploadTiming')}</Label>
|
||||
<Select
|
||||
value={hamlog.upload_mode === 'delayed' ? 'delayed' : 'immediate'}
|
||||
onValueChange={(v) => setHamlog({ upload_mode: v })}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-64"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="immediate">{t('es.immediate')}</SelectItem>
|
||||
<SelectItem value="delayed">{t('es.delayed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" onClick={testHamlog} disabled={hamlogTesting}>
|
||||
<UploadCloud className="size-3.5" /> {hamlogTesting ? t('es.testing') : t('es.hamlogCheckKey')}
|
||||
</Button>
|
||||
{hamlogTest && (
|
||||
<span className={cn('text-xs', hamlogTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{hamlogTest.msg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : extSvcTab === 'cloudlog' ? (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
|
||||
|
||||
@@ -356,7 +356,7 @@ const en: Dict = {
|
||||
'cat.rotatorOk': "Packet sent — antenna should swing to 0° (north). If it didn't, check PstRotator host/port and that PstRotator's UDP listener is enabled.",
|
||||
'cat.ubOk': 'Connected — the antenna responded with a status frame.',
|
||||
// External services (repeated labels)
|
||||
'es.autoUpload': 'Automatic upload on new QSO', 'es.uploadTiming': 'Upload timing', 'es.immediate': 'Immediate', 'es.delayed': 'Delayed (1–2 min, lets you fix mistakes)', 'es.onClose': 'On app close (batch)', 'es.testConn': 'Test connection', 'es.testing': 'Testing…', 'es.password': 'Password', 'es.showPass': 'Show password', 'es.hidePass': 'Hide password', 'es.apiKey': 'API key', 'es.cloudlogUrl': 'Instance URL', 'es.cloudlogUrlPh': 'https://log.example.com', 'es.cloudlogStationId': 'Station ID', 'es.cloudlogKeyPh': 'read/write API key', 'es.cloudlogHint': 'Cloudlog and Wavelog are self-hosted: give the address of YOUR instance (an IP works on a LAN). The API key is created under Account → API Keys and must be read/write; the station ID is the number of the station location the QSOs are filed under (Station Locations page). Duplicates are rejected by the server, so re-sending a QSO is harmless.', 'es.forceCall': 'Force station callsign', 'es.accountEmail': 'Account email', 'es.logbookCall': 'Logbook callsign',
|
||||
'es.autoUpload': 'Automatic upload on new QSO', 'es.uploadTiming': 'Upload timing', 'es.immediate': 'Immediate', 'es.delayed': 'Delayed (1–2 min, lets you fix mistakes)', 'es.onClose': 'On app close (batch)', 'es.testConn': 'Test connection', 'es.testing': 'Testing…', 'es.password': 'Password', 'es.showPass': 'Show password', 'es.hidePass': 'Hide password', 'es.apiKey': 'API key', 'es.cloudlogUrl': 'Instance URL', 'es.cloudlogUrlPh': 'https://log.example.com', 'es.cloudlogStationId': 'Station ID', 'es.cloudlogKeyPh': 'read/write API key', 'es.hamlogHint': 'Your personal API key, created at', 'es.hamlogCheckKey': 'Check the key', 'es.hamlogKeyOk': 'Key is valid', 'es.hamlogKeyOkCall': 'Key is valid — account {call}', 'es.cloudlogHint': 'Cloudlog and Wavelog are self-hosted: give the address of YOUR instance (an IP works on a LAN). The API key is created under Account → API Keys and must be read/write; the station ID is the number of the station location the QSOs are filed under (Station Locations page). Duplicates are rejected by the server, so re-sending a QSO is harmless.', 'es.forceCall': 'Force station callsign', 'es.accountEmail': 'Account email', 'es.logbookCall': 'Logbook callsign',
|
||||
// External-services placeholders + HRDLOG / eQSL / LoTW / POTA tabs
|
||||
'es.qrzApiPh': 'QRZ.com logbook API key (XXXX-XXXX-XXXX-XXXX)', 'es.forceCallPh': 'e.g. F4BPO — optional', 'es.callDefaultPh': "defaults to the active profile's callsign",
|
||||
'es.clubEmailPh': 'your Club Log account email', 'es.clubPwPh': 'Club Log account password',
|
||||
@@ -834,7 +834,7 @@ const fr: Dict = {
|
||||
'cat.omnirigHint': "Configure d'abord ton poste (port COM, débit, modèle) dans l'interface de réglages d'OmniRig. OpsLog lira le slot Rig que tu choisis ici. Mets le délai CAT au-dessus de 0 si ton poste perd des commandes envoyées coup sur coup (certains anciens Kenwood/Yaesu). OmniRig ne rapporte qu'un « DIG » générique pour les modes numériques — le mode numérique par défaut est le mode précis qu'OpsLog affichera (et loggera).",
|
||||
'cat.rotatorOk': "Paquet envoyé — l'antenne devrait tourner vers 0° (nord). Sinon, vérifie l'hôte/port PstRotator et que l'écouteur UDP de PstRotator est activé.",
|
||||
'cat.ubOk': "Connecté — l'antenne a répondu avec une trame de statut.",
|
||||
'es.autoUpload': 'Envoi automatique à chaque nouveau QSO', 'es.uploadTiming': "Moment de l'envoi", 'es.immediate': 'Immédiat', 'es.delayed': 'Différé (1–2 min, permet de corriger les erreurs)', 'es.onClose': "À la fermeture (par lot)", 'es.testConn': 'Tester la connexion', 'es.testing': 'Test…', 'es.password': 'Mot de passe', 'es.showPass': 'Afficher le mot de passe', 'es.hidePass': 'Masquer le mot de passe', 'es.apiKey': 'Clé API', 'es.cloudlogUrl': "Adresse de l'instance", 'es.cloudlogUrlPh': 'https://log.exemple.fr', 'es.cloudlogStationId': 'ID de station', 'es.cloudlogKeyPh': 'clé API lecture/écriture', 'es.cloudlogHint': "Cloudlog et Wavelog sont auto-hébergés : indiquez l'adresse de VOTRE instance (une IP convient en réseau local). La clé API se crée dans Compte → API Keys et doit être en lecture/écriture ; l'ID de station est le numéro de l'emplacement sous lequel les QSO sont classés (page Station Locations). Les doublons sont refusés par le serveur, renvoyer un QSO est donc sans risque.", 'es.forceCall': "Forcer l'indicatif de station", 'es.accountEmail': 'E-mail du compte', 'es.logbookCall': 'Indicatif du carnet',
|
||||
'es.autoUpload': 'Envoi automatique à chaque nouveau QSO', 'es.uploadTiming': "Moment de l'envoi", 'es.immediate': 'Immédiat', 'es.delayed': 'Différé (1–2 min, permet de corriger les erreurs)', 'es.onClose': "À la fermeture (par lot)", 'es.testConn': 'Tester la connexion', 'es.testing': 'Test…', 'es.password': 'Mot de passe', 'es.showPass': 'Afficher le mot de passe', 'es.hidePass': 'Masquer le mot de passe', 'es.apiKey': 'Clé API', 'es.cloudlogUrl': "Adresse de l'instance", 'es.cloudlogUrlPh': 'https://log.exemple.fr', 'es.cloudlogStationId': 'ID de station', 'es.cloudlogKeyPh': 'clé API lecture/écriture', 'es.hamlogHint': 'Ta clé API personnelle, à créer sur', 'es.hamlogCheckKey': 'Vérifier la clé', 'es.hamlogKeyOk': 'Clé valide', 'es.hamlogKeyOkCall': 'Clé valide — compte {call}', 'es.cloudlogHint': "Cloudlog et Wavelog sont auto-hébergés : indiquez l'adresse de VOTRE instance (une IP convient en réseau local). La clé API se crée dans Compte → API Keys et doit être en lecture/écriture ; l'ID de station est le numéro de l'emplacement sous lequel les QSO sont classés (page Station Locations). Les doublons sont refusés par le serveur, renvoyer un QSO est donc sans risque.", 'es.forceCall': "Forcer l'indicatif de station", 'es.accountEmail': 'E-mail du compte', 'es.logbookCall': 'Indicatif du carnet',
|
||||
// Placeholders services externes + onglets HRDLOG / eQSL / LoTW / POTA
|
||||
'es.qrzApiPh': 'Clé API du logbook QRZ.com (XXXX-XXXX-XXXX-XXXX)', 'es.forceCallPh': 'p. ex. F4BPO — optionnel', 'es.callDefaultPh': "par défaut : l'indicatif du profil actif",
|
||||
'es.clubEmailPh': 'e-mail de ton compte Club Log', 'es.clubPwPh': 'mot de passe du compte Club Log',
|
||||
|
||||
@@ -26,7 +26,7 @@ export type RowColorSettings = {
|
||||
// otherwise keep striping underneath whatever we do per row.
|
||||
const ZEBRA_DEFAULT = 'color-mix(in srgb, var(--muted) 40%, var(--card))';
|
||||
|
||||
export const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz'] as const;
|
||||
export const CHANNELS = ['qsl', 'lotw', 'eqsl', 'qrz', 'hamlog'] as const;
|
||||
|
||||
// The two QSO fields behind each channel. QRZ.com and Club Log call theirs an
|
||||
// "upload status" rather than a QSL flag, but they carry the same Y / R letters.
|
||||
@@ -37,6 +37,24 @@ const FIELDS: Record<string, { sent: string; rcvd: string }> = {
|
||||
qrz: { sent: 'qrzcom_qso_upload_status', rcvd: 'qrzcom_qso_download_status' },
|
||||
};
|
||||
|
||||
// HAMLOG.online has no column of its own: the ADIF standard names a field for
|
||||
// hamlog.EU and none for hamlog.ONLINE, so its state lives in the extras — see
|
||||
// internal/award/award.go, which reads the same keys for award confirmations.
|
||||
// One vocabulary, two readers.
|
||||
const HAMLOG_SENT = 'APP_OPSLOG_HAMLOG_SENT';
|
||||
const HAMLOG_RCVD = ['APP_OPSLOG_HAMLOG_QSL', 'APP_HAMLOG_QSL', 'APP_HAMLOGONLINE_QSL', 'HAMLOG_QSL_RCVD'];
|
||||
|
||||
// hamlogState reads a row's extras. Any value that is not an explicit "no"
|
||||
// counts, because their export could carry a date or a match id rather than Y.
|
||||
function hamlogState(q: any, which: 'sent' | 'rcvd'): boolean {
|
||||
const e = (q?.extras ?? {}) as Record<string, string>;
|
||||
const keys = which === 'sent' ? [HAMLOG_SENT] : HAMLOG_RCVD;
|
||||
return keys.some((k) => {
|
||||
const v = String(e[k] ?? '').trim();
|
||||
return v !== '' && v.toUpperCase() !== 'N' && v.toUpperCase() !== 'NO';
|
||||
});
|
||||
}
|
||||
|
||||
// ADIF QSL fields are single letters. Y is the only one that means "yes";
|
||||
// R (requested) and Q (queued) mean it has not gone out yet — a different state,
|
||||
// and the one an operator looks for when deciding what to send.
|
||||
@@ -55,14 +73,18 @@ function ruleMatches(q: any, r: RowColorRule): boolean {
|
||||
const cs = chansOf(r);
|
||||
switch (r.id) {
|
||||
case 'confirmed':
|
||||
return cs.some((c) => yes(q[FIELDS[c]?.rcvd]));
|
||||
return cs.some((c) => (c === 'hamlog' ? hamlogState(q, 'rcvd') : yes(q[FIELDS[c]?.rcvd])));
|
||||
case 'sent':
|
||||
return cs.some((c) => yes(q[FIELDS[c]?.sent]));
|
||||
return cs.some((c) => (c === 'hamlog' ? hamlogState(q, 'sent') : yes(q[FIELDS[c]?.sent])));
|
||||
case 'to_send':
|
||||
return cs.some((c) => owed(q[FIELDS[c]?.sent]));
|
||||
// Nothing to request from HAMLOG: a QSO is either uploaded or it is not,
|
||||
// there is no "card asked for" state to be owed.
|
||||
return cs.some((c) => c !== 'hamlog' && owed(q[FIELDS[c]?.sent]));
|
||||
case 'worked':
|
||||
// The catch-all: nothing sent, nothing asked for, nothing back.
|
||||
return !CHANNELS.some((c) => yes(q[FIELDS[c].rcvd]) || yes(q[FIELDS[c].sent]) || owed(q[FIELDS[c].sent]));
|
||||
return !CHANNELS.some((c) => (c === 'hamlog'
|
||||
? hamlogState(q, 'rcvd') || hamlogState(q, 'sent')
|
||||
: yes(q[FIELDS[c].rcvd]) || yes(q[FIELDS[c].sent]) || owed(q[FIELDS[c].sent])));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user