Skip to content
/ app-polo Public
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"versionName": "October '24",
"private": true,
"scripts": {
"android": "react-native run-android",
"android": "react-native run-android --mode alphaDebug",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
Expand Down
152 changes: 152 additions & 0 deletions src/extensions/activities/skcc/SKCCExtension.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useEffect, useRef, useState } from 'react'
import { Button, Dialog, Text } from 'react-native-paper'
import { dbExecute } from '../../../store/db/db'
import { findRef } from '../../../tools/refTools'
import { Ham2kDialog } from '../../../screens/components/Ham2kDialog'
import { Info } from './SKCCInfo'
import { loadDataFile, removeDataFile } from "../../../store/dataFiles/actions/dataFileFS";
import { registerSKCCMembershipData } from "./SKCCMembershipData";
import { useDebounce } from "../../../hooks/useDebounce";
import ThemedTextInput from '../../../screens/components/ThemedTextInput'

const Extension = {
...Info,
cateogry: 'other',
enabledByDefault: false,
alwaysEnabled: false,
onActivationDispatch: ({ registerHook }) => async (dispatch) => {
registerHook('activity', { hook: ActivityHook })
registerHook(`ref:${Info.activationType}`, { hook: ReferenceHandler })
registerSKCCMembershipData()

await dispatch(loadDataFile('skcc-membership', { noticesInsteadOfFetch: true }))
},
onDeactivationDispatch: () => async (dispatch) => {
await dispatch(removeDataFile('skcc-membership'))
}
}
export default Extension

const ReferenceHandler = {
...Info,

suggestOperationTitle: () => {
return { title: 'Straight Key Century Club' }
}
}

const ActivityHook = {
...Info,

mainExchangeForQSO: MainExchangeForQSO
}

function MainExchangeForQSO({ qso, operation, themeColor, updateQSO, handleFieldChange }) {
const debouncedQso = useDebounce(qso)
const call = debouncedQso?.their?.call
const skcc = debouncedQso?.their?.skcc
const [proposedGuess, setProposedGuess] = useState()
const confirmedGuess = useRef()

useEffect(() => {
if (!(call && call !== '') && !(skcc && skcc !== '')) {
return
}

if (call === confirmedGuess?.current?.call && skcc === confirmedGuess?.current?.skccNr) {
return
}

async function fetch() {
const result = call && call !== confirmedGuess?.current?.call
? await dbExecute('SELECT * FROM skccMembers WHERE call = ?', [call])
: await dbExecute('SELECT * FROM skccMembers WHERE skcc = ?', [skcc])
if (!ignore) {
if (result.rows.length === 1) {
const row = result.rows.item(0)
setProposedGuess(row)
}
}
}

let ignore = false
fetch()
return () => {
ignore = true
}
}, [call, skcc, confirmedGuess, setProposedGuess])

const handleLeaveAsIs = () => {
setProposedGuess(undefined)
}
const handleClearFields = () => {
confirmedGuess.current = undefined
setProposedGuess(undefined)
updateQSO({ their: { call: undefined, name: undefined, skcc: undefined, state: undefined, comment: undefined } })
}
const handleOverwriteFields = () => {
confirmedGuess.current = proposedGuess
setProposedGuess(undefined)
const comment = `SKCC: ${proposedGuess?.skccNr} - ${proposedGuess?.name} - ${proposedGuess?.spc}`
updateQSO({ their: { call: proposedGuess?.call, name: proposedGuess?.name, skcc: proposedGuess?.skccNr, state: proposedGuess?.spc }, comment })
}

const fields = proposedGuess ? [
<ConfirmClearSKCCFieldsDialog
key="skcc-clear-dialog"
newFields={proposedGuess}
onClear={handleClearFields}
onLeaveAsIs={handleLeaveAsIs}
onOverwrite={handleOverwriteFields}
/>
] : []
if (findRef(operation, Info.activationType)) {
fields.push(
<ThemedTextInput
key="skcc"
label="SKCC"
themeColor={themeColor}
value={qso?.their?.skcc ?? ''}
placeholder={qso?.their?.skcc ?? ''}
uppercase={true}
noSpaces={true}
fieldId={'skcc'}
onChange={handleFieldChange}
/>
)
fields.push(
<ThemedTextInput
key="name"
label="Name"
themeColor={themeColor}
value={qso?.their?.name ?? ''}
placeholder={qso?.their?.name ?? ''}
uppercase={false}
noSpaces={false}
fieldId={'name'}
onChange={handleFieldChange}
/>
)
}

return fields
}

function ConfirmClearSKCCFieldsDialog({ newFields, onLeaveAsIs, onOverwrite, onClear }) {
return (
<Ham2kDialog visible={true} onDismiss={onLeaveAsIs}>
<Dialog.Title style={{ textAlign: 'center' }}>Overwrite SKCC Fields</Dialog.Title>
<Dialog.Content>
<Text variant="bodyMedium" style={{ textAlign: 'center' }}>Updating SKCC fields may overwrite already entered data for this QSOs</Text>
</Dialog.Content>
<Dialog.Content>
<Text variant="bodyMedium" style={{ textAlign: 'center' }}>{`SKCC: ${newFields.skccNr}, Name: ${newFields.name}, SPC: ${newFields.spc}`}</Text>
</Dialog.Content>
<Dialog.Actions style={{ justifyContent: 'space-between' }}>
<Button onPress={onOverwrite}>Overwrite Fields</Button>
<Button onPress={onClear}>Clear Fields</Button>
<Button onPress={onLeaveAsIs}>Leave As Is</Button>
</Dialog.Actions>
</Ham2kDialog>
)
}
9 changes: 9 additions & 0 deletions src/extensions/activities/skcc/SKCCInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const Info = {
key: 'skcc',
icon: 'key-arrow-right',
name: 'Straight Key Centry Club',
shortName: 'SKCC',
infoURL: 'https://www.skccgroup.com/',
refType: 'skcc',
activationType: 'skccActivation'
}
123 changes: 123 additions & 0 deletions src/extensions/activities/skcc/SKCCMembershipData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { fmtNumber, fmtPercent } from '@ham2k/lib-format-tools'

import { fetchAndProcessURL } from "../../../store/dataFiles/actions/dataFileFS"
import { registerDataFile } from '../../../store/dataFiles'
import { database, dbExecute } from '../../../store/db/db'

export function registerSKCCMembershipData() {
registerDataFile({
key: 'skcc-membership',
name: 'SKCC: Membership',
description: 'Database of all SKCC members',
infoURL: 'https://www.skccgroup.com/',
icon: 'file-key-outline',
maxAgeInDays: 2,
enabledByDefault: false,
fetch: async (args) => {
const { key, definition, options } = args

options.onStatus && await options.onStatus({ key, definition, status: 'progress', progress: 'Downloading raw data' })

const url = 'https://www.skccgroup.com/membership_data/skccdata.txt'

return await fetchAndProcessURL({
...args,
url,
process: async (body) => {
const lines = body.split('\n')
const headers = parseSKCCHeaders(lines.shift())

const db = await database()
await dbExecute('UPDATE skccMembers SET updated = 0')

const inserts = lines
.map(line => parseSKCCRow(line, headers))
.filter(row => row.SKCCNR && row.SKCCNR !== '')
.map(row => {
const joinDate = parseSKCCDate(row.JOINDATE)
const centDate = parseSKCCDate(row.CENTDATE)
const tribDate = parseSKCCDate(row.TRIBDATE)
const tx8Date = parseSKCCDate(row.TX8DATE)
const senDate = parseSKCCDate(row.SENDATE)
const query = `
INSERT into skccMembers
(skcc, skccNr, call, name, qth, spc, oldCall, dxCode, joinDate, centDate, tribDate, tx8Date, senDate, dxEntity, updated)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
ON CONFLICT DO
UPDATE SET
skccNr = ?, call = ?, name = ?, qth = ?, spc = ?, oldCall = ?, dxCode = ?, joinDate = ?, centDate = ?, tribDate = ?, tx8Date = ?, senDate = ?, dxEntity = ?, updated = 1
`
const args = [row.SKCC, row.SKCCNR, row.CALL, row.NAME, row.QTH, row.SPC, row.OLDCALL, row.DXCODE, joinDate, centDate, tribDate, tx8Date, senDate, row.DXENTITY]
return { query, args }
})
const totalMembers = inserts.length

const startTime = Date.now()
let processedMembers = 0
while (inserts.length > 0) {
await new Promise((resolve, reject) => {
db.transaction(transaction => {
inserts.splice(0, 797).forEach(({ query, args }) => {
transaction.executeSql(query, args, (_, resultSet) => { processedMembers += resultSet.rowsAffected })
})
}, (error) => {
reject(error)
}, () => {
resolve()
})
})

options.onStatus && await options.onStatus({
key,
definition,
status: 'progress',
progress: `Loaded \`${fmtNumber(processedMembers)}\` members.\n\n\`${fmtPercent(Math.min(processedMembers / totalMembers, 1), 'integer')}\` • ${fmtNumber((totalMembers - processedMembers) * ((Date.now() - startTime) / 1000) / processedMembers, 'oneDecimal')} seconds left.`
})
}

await dbExecute('DELETE FROM skccMembers WHERE updated = 0')

return { totalMembers }
}
})
},
onRemove: async () => {
await dbExecute('DELETE FROM skccMembers')
}
})
}

function parseSKCCHeaders(row) {
if (!row) {
return []
}

return row.split('|')
}

function parseSKCCRow(row, headers) {
const parts = row.split('|')
const obj = {}

headers.forEach((column, index) => {
obj[column] = parts[index]
})

return obj
}

function parseSKCCDate(date) {
if (!date) {
return
}

const parts = date.split(' ')
if (parts.length !== 3) {
console.warn(`Unexpected SKCC date format: ${date}`)
return
}

const [day, month, year] = parts
return [year, month, day].join('-')
}
2 changes: 2 additions & 0 deletions src/extensions/loadExtensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import CallNotesExtension from './data/call-notes/CallNotesExtension'
import CallHistoryExtension from './data/call-history/CallHistoryExtension'
import QRZExtension from './data/qrz/QRZExtension'
import SatellitesExtension from './activities/satellites/SatellitesExtension'
import SKCCExtension from './activities/skcc/SKCCExtension'

import RadioCommands from './commands/RadioCommands'
import TimeCommands from './commands/TimeCommands'
Expand All @@ -48,6 +49,7 @@ const loadExtensions = () => async (dispatch, getState) => {
registerExtension(ECAExtension)
registerExtension(ELAExtension)
registerExtension(SiOTAExtentsion)
registerExtension(SKCCExtension)

registerExtension(RadioCommands)
registerExtension(TimeCommands)
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/useDebounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState, useEffect } from 'react'

export function useDebounce(value, delayMs = 500) {
const [debounced, setDebounced] = useState(value)

useEffect(() => {
const handler = setTimeout(() => {
setDebounced(value)
}, delayMs)

return () => {
clearTimeout(handler)
}
}, [value, delayMs])

return debounced
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export default function LoggingPanel ({ style, operation, vfo, qsos, sections, a
updateQSO({ startOnMillis: value, _manualTime: true })
} else if (fieldId === 'state') {
updateQSO({ their: { state: value } })
} else if (fieldId === 'name') {
updateQSO({ their: { name: value } })
} else if (fieldId === 'skcc') {
updateQSO({ their: { skcc: value } })
} else if (fieldId === 'power') {
updateQSO({ power: value })
if (qso?._isNew) dispatch(setVFO({ power: value }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const MainExchangePanel = ({
findHooks('activity').filter(activity => activity.mainExchangeForQSO).forEach(activity => {
fields = fields.concat(
activity.mainExchangeForQSO(
{ qso, operation, vfo, settings, styles, themeColor, onSubmitEditing, setQSO, updateQSO, onSpace: spaceHandler, refStack, focusedRef }
{ qso, operation, vfo, settings, styles, themeColor, onSubmitEditing, setQSO, updateQSO, onSpace: spaceHandler, refStack, focusedRef, handleFieldChange }
) || []
)
})
Expand Down
34 changes: 33 additions & 1 deletion src/store/db/createTables.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,39 @@ export async function createTables (db) {
await dbExecute('UPDATE version SET version = 2', [], { db })
}

if (version === 2) {
if (version < 3) {
console.log('createTables -- creating version 3')
/**
* skcc is the standalone SKCC number, e.g. 1234
* skccNr is the SKCC number plus any (C, T, S) award earned, e.g. 1234T
*/
await dbExecute(`
CREATE TABLE IF NOT EXISTS skccMembers (
skcc TEXT NOT NULL,
skccNr TEXT,
call TEXT,
name TEXT,
qth TEXT,
spc TEXT,
oldCall TEXT,
dxCode INTEGER,
joinDate DATE,
centDate DATE,
tribDate DATE,
tx8Date DATE,
senDate DATE,
dxEntity TEXT,
updated INTEGER,
PRIMARY KEY (skcc)
)`, [], { db })

await dbExecute(`
CREATE INDEX IF NOT EXISTS
idx_call ON skccMembers (call)`, [], { db })
await dbExecute('UPDATE version SET version = 3', [], { db })
}

if (version === 3) {
// console.log('createTables -- using version 1')
}
}
1 change: 1 addition & 0 deletions src/tools/qsonToADIF.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function adifFieldsForOneQSO (qso, operation, common, timeOfffset = 0) {
{ STATION_CALLSIGN: qso.our.call ?? common.stationCall },
{ OPERATOR: qso.our.operatorCall ?? common.operatorCall ?? qso.our.call ?? common.stationCall },
{ NOTES: qso.notes },
{ COMMENT: qso.comment },
{ COMMENTS: qso.notes },
{ GRIDSQUARE: qso.their?.grid ?? qso.their?.guess?.grid },
{ MY_GRIDSQUARE: qso?.our?.grid ?? common.grid },
Expand Down