feat(spot): put the award references in the spot comment
After the mode, which is the order a cluster line is read in: "CW POTA FR-11553". Two sources, because the QSO can be at either stage. While it is being typed the entry panel holds the references as "CODE@REF;CODE@REF"; once logged they live on the row as the materialised award_refs. The Send Spot window takes the entry's when there are any and falls back to the last logged QSO — the same fallback the callsign and frequency defaults already use — and picking a QSO from the Latest list fills in that QSO's own. A self-spot is the opposite case and gets its own builder: it announces OUR station, so it carries MY_POTA_REF and friends. Using the QSO's award references there would spot us with the park number of the station we just worked, announcing us from somewhere we are not. Both cap at the 30 characters a cluster node keeps, and add a reference whole or not at all — a truncated park number is worse than none, since nobody can act on it, and it still costs everyone who reads the spot. Only the comment we BUILD is held to that; what the operator types is their own business.
This commit is contained in:
+18
-1
@@ -55,7 +55,7 @@ import {
|
||||
QSLViaRepairStatus, RepairQSLVia, DismissQSLViaRepair,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs } from '@/lib/awardRefs';
|
||||
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList } from '@/lib/awardRefs';
|
||||
import { EventsOn, BrowserOpenURL, WindowMinimise, WindowToggleMaximise, WindowIsMaximised, Quit } from '../wailsjs/runtime/runtime';
|
||||
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
|
||||
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -2086,6 +2086,19 @@ export default function App() {
|
||||
const [showScp, setShowScp] = useState(() => localStorage.getItem('opslog.showScp') !== '0');
|
||||
const [showBeamOnMap, setShowBeamOnMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
|
||||
|
||||
// Award references offered to the Send Spot window, as cluster-ready strings.
|
||||
//
|
||||
// The entry panel holds them as "CODE@REF;CODE@REF" while the QSO is being
|
||||
// typed; once it is logged they live on the row as the materialised award_refs
|
||||
// object. Take the entry's when there is one — spotting usually happens with
|
||||
// the station still in the field — and fall back to the last logged QSO, which
|
||||
// is where the callsign and frequency defaults come from too.
|
||||
const spotEntryRefs = useMemo(() => {
|
||||
const entry = spotRefList(parseManualRefs(details.award_refs ?? ''));
|
||||
if (entry.length) return entry;
|
||||
return spotRefList(parseAwardRefs(qsos[0]?.award_refs));
|
||||
}, [details.award_refs, qsos]);
|
||||
|
||||
// Award code → scanned field (e.g. POTA→pota_ref, WWFF→wwff). Used to route
|
||||
// picked award references to the QSO field/extras each award actually reads.
|
||||
const awardFieldRef = useRef<Record<string, string>>({});
|
||||
@@ -7216,6 +7229,9 @@ export default function App() {
|
||||
: (qsos[0]?.freq_hz ? Math.round((qsos[0].freq_hz / 1000) * 10) / 10 : 0)
|
||||
}
|
||||
defaultMode={mode || qsos[0]?.mode || ''}
|
||||
// Award references: the ones on the QSO being entered, else those already
|
||||
// logged for the last QSO — the same source the callsign above falls back to.
|
||||
defaultRefs={spotEntryRefs}
|
||||
targetName={
|
||||
clusterServers
|
||||
.filter((s) => s.enabled)
|
||||
@@ -7226,6 +7242,7 @@ export default function App() {
|
||||
freqKHz: q.freq_hz ? Math.round((q.freq_hz / 1000) * 10) / 10 : 0,
|
||||
mode: q.mode ?? '',
|
||||
band: q.band,
|
||||
refs: spotRefList(parseAwardRefs(q.award_refs)),
|
||||
}))}
|
||||
onSend={async (call, freqKHz, comment) => {
|
||||
await SendClusterSpot(call, freqKHz, comment);
|
||||
|
||||
@@ -13,6 +13,32 @@ export interface RecentSpotQSO {
|
||||
freqKHz: number;
|
||||
mode: string;
|
||||
band?: string;
|
||||
// Award references logged for this QSO, each one complete and ready to read
|
||||
// on a cluster line ("POTA FR-11553"). Empty for an ordinary contact.
|
||||
refs?: string[];
|
||||
}
|
||||
|
||||
// SPOT_COMMENT_MAX is the DX cluster comment field: 30 characters, and nodes
|
||||
// truncate what does not fit without saying so. Only the comment BUILT here is
|
||||
// held to it — whatever the operator types is their own business.
|
||||
const SPOT_COMMENT_MAX = 30;
|
||||
|
||||
// spotComment puts the award references after the mode, the order a cluster
|
||||
// line is read in: "CW POTA FR-11553".
|
||||
//
|
||||
// A reference goes in whole or not at all. Truncation would leave "CW POTA" or
|
||||
// half a park number on the air, which is worse than saying nothing: a spot
|
||||
// nobody can act on still costs everyone who reads it.
|
||||
export function spotComment(mode: string, refs?: string[]): string {
|
||||
let out = (mode || '').trim();
|
||||
for (const ref of refs ?? []) {
|
||||
const one = ref.trim();
|
||||
if (!one) continue;
|
||||
const next = out ? `${out} ${one}` : one;
|
||||
if (next.length > SPOT_COMMENT_MAX) continue; // a shorter one may still fit
|
||||
out = next;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -23,6 +49,9 @@ interface Props {
|
||||
defaultCall: string;
|
||||
defaultFreqKHz: number;
|
||||
defaultMode: string;
|
||||
// Award references on the QSO being entered (or on the last logged one),
|
||||
// appended to the comment after the mode.
|
||||
defaultRefs?: string[];
|
||||
// Master cluster name, shown so the user knows where the spot goes.
|
||||
targetName?: string;
|
||||
recent: RecentSpotQSO[];
|
||||
@@ -33,7 +62,7 @@ interface Props {
|
||||
// the master cluster: callsign + frequency (kHz) + a free message (defaults
|
||||
// to the mode). A "Latest QSOs" list lets the operator one-click a recent
|
||||
// contact into the form.
|
||||
export function SendSpotModal({ open, onClose, defaultCall, defaultFreqKHz, defaultMode, targetName, recent, onSend }: Props) {
|
||||
export function SendSpotModal({ open, onClose, defaultCall, defaultFreqKHz, defaultMode, defaultRefs, targetName, recent, onSend }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [call, setCall] = useState('');
|
||||
const [freqKHz, setFreqKHz] = useState('');
|
||||
@@ -48,12 +77,12 @@ export function SendSpotModal({ open, onClose, defaultCall, defaultFreqKHz, defa
|
||||
if (!open) return;
|
||||
setCall((defaultCall || '').toUpperCase());
|
||||
setFreqKHz(defaultFreqKHz > 0 ? trimKHz(defaultFreqKHz) : '');
|
||||
setMessage(defaultMode || '');
|
||||
setMessage(spotComment(defaultMode, defaultRefs));
|
||||
setError('');
|
||||
setOk(false);
|
||||
// Focus the freq if the call is already known, else the call.
|
||||
setTimeout(() => callRef.current?.focus(), 50);
|
||||
}, [open, defaultCall, defaultFreqKHz, defaultMode]);
|
||||
}, [open, defaultCall, defaultFreqKHz, defaultMode, defaultRefs]);
|
||||
|
||||
async function send() {
|
||||
const c = call.trim().toUpperCase();
|
||||
@@ -77,7 +106,7 @@ export function SendSpotModal({ open, onClose, defaultCall, defaultFreqKHz, defa
|
||||
function pick(q: RecentSpotQSO) {
|
||||
setCall(q.callsign.toUpperCase());
|
||||
if (q.freqKHz > 0) setFreqKHz(trimKHz(q.freqKHz));
|
||||
if (q.mode) setMessage(q.mode);
|
||||
setMessage(spotComment(q.mode, q.refs));
|
||||
setError('');
|
||||
}
|
||||
|
||||
|
||||
@@ -154,3 +154,20 @@ export function buildAwardRefs(qso: any, pickable: Array<{ code: string; field:
|
||||
}
|
||||
return out.join(';');
|
||||
}
|
||||
|
||||
// spotRefList turns a code→ref map into the strings a DX-cluster comment wants:
|
||||
// ["POTA FR-11553", "IOTA EU-064"]. A code carrying several references (a
|
||||
// two-fer park activation) yields one entry per reference, so a spot can carry
|
||||
// the one that fits and drop the rest rather than emitting a truncated pair.
|
||||
//
|
||||
// Shortest first: cluster comments are 30 characters, and when they do not all
|
||||
// fit, more references beat fewer.
|
||||
export function spotRefList(byCode: Record<string, string>): string[] {
|
||||
const out: string[] = [];
|
||||
for (const [code, refs] of Object.entries(byCode ?? {})) {
|
||||
for (const ref of String(refs).split(',').map((s) => s.trim()).filter(Boolean)) {
|
||||
out.push(`${code.toUpperCase()} ${ref.toUpperCase()}`);
|
||||
}
|
||||
}
|
||||
return out.sort((a, b) => a.length - b.length || a.localeCompare(b));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user