Browse Source

Editor: better publishing

imwald
Nuša Pukšič 4 days ago
parent
commit
bdcec143f2
  1. 97
      assets/controllers/nostr/nostr_publish_controller.js
  2. 96
      assets/controllers/nostr/nostr_single_sign_controller.js
  3. 90
      src/Controller/ArticleController.php
  4. 7
      src/Controller/AuthorController.php
  5. 4
      src/Service/NostrClient.php

97
assets/controllers/nostr/nostr_publish_controller.js

@ -248,14 +248,19 @@ export default class extends Controller { @@ -248,14 +248,19 @@ export default class extends Controller {
this.showStatus('Publishing article...');
// Send to backend
await this.sendToBackend(signedEvent, this.collectFormData());
const result = await this.sendToBackend(signedEvent, this.collectFormData());
// Show appropriate success message
if (result.isDraft) {
this.showSuccess('Draft saved successfully!');
// Stay on editor page for drafts - just show success message
} else {
this.showSuccess('Article published successfully!');
// Optionally redirect after successful publish
// Redirect to article page after short delay
setTimeout(() => {
window.location.href = `/article/d/${encodeURIComponent(nostrEvent.tags?.find(t => t[0] === 'd')?.[1] || '')}`;
window.location.href = `/article/d/${encodeURIComponent(result.slug || nostrEvent.tags?.find(t => t[0] === 'd')?.[1] || '')}`;
}, 2000);
}
} catch (error) {
console.error('Publishing error:', error);
@ -493,7 +498,14 @@ export default class extends Controller { @@ -493,7 +498,14 @@ export default class extends Controller {
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
const result = await response.json();
// Display relay results if available
if (result.relayResults) {
this.displayRelayResults(result.relayResults);
}
return result;
}
generateSlug(title) {
@ -508,6 +520,81 @@ export default class extends Controller { @@ -508,6 +520,81 @@ export default class extends Controller {
.replace(/^-|-$/g, '');
}
displayRelayResults(relayResults) {
console.log('[nostr-publish] Relay results:', relayResults);
// Handle error case
if (relayResults.error) {
window.showToast(`Relay error: ${relayResults.error}`, 'warning', 6000);
return;
}
// Parse relay results from backend
if (!Array.isArray(relayResults) || relayResults.length === 0) {
window.showToast('Published to relays (status unknown)', 'info', 4000);
return;
}
// Count successes, failures, and unknowns
let successCount = 0;
let failureCount = 0;
let unknownCount = 0;
const failedRelays = [];
const unknownRelays = [];
relayResults.forEach((result) => {
const relayUrl = result.relay || 'Unknown relay';
if (result.success) {
successCount++;
} else if (result.type === 'auth') {
// AUTH responses are unknown status - not confirmed but not failed
unknownCount++;
unknownRelays.push(relayUrl);
} else {
// Other non-success responses are failures
failureCount++;
failedRelays.push(relayUrl);
}
});
// Show summary toast
if (successCount > 0 && failureCount === 0 && unknownCount === 0) {
// Perfect success
window.showToast(`✓ Published to ${successCount} relay${successCount > 1 ? 's' : ''}`, 'success', 5000);
} else if (successCount > 0 && unknownCount > 0 && failureCount === 0) {
// Success with some unknowns
window.showToast(`✓ Published to ${successCount} relay${successCount > 1 ? 's' : ''}, ${unknownCount} unknown`, 'success', 6000);
// Show details about unknowns
if (unknownRelays.length > 0) {
setTimeout(() => {
window.showToast(`Unknown status: ${unknownRelays.join(', ')}`, 'info', 8000);
}, 500);
}
} else if (successCount > 0 && failureCount > 0) {
// Mixed success and failure
const statusParts = [`${successCount} success`];
if (unknownCount > 0) statusParts.push(`${unknownCount} unknown`);
statusParts.push(`${failureCount} failed`);
window.showToast(`Published: ${statusParts.join(', ')}`, 'warning', 6000);
// Show details about failures
setTimeout(() => {
if (failedRelays.length > 0) {
window.showToast(`Failed: ${failedRelays.join(', ')}`, 'danger', 8000);
}
if (unknownRelays.length > 0) {
setTimeout(() => {
window.showToast(`Unknown: ${unknownRelays.join(', ')}`, 'info', 8000);
}, 500);
}
}, 500);
} else if (failureCount > 0 || unknownCount > 0) {
// No successes
window.showToast(`✗ Publishing uncertain (${unknownCount} unknown, ${failureCount} failed)`, 'warning', 8000);
}
}
showStatus(message) {
// Use toast system if available, otherwise fallback to status target
if (typeof window.showToast === 'function') {

96
assets/controllers/nostr/nostr_single_sign_controller.js

@ -138,17 +138,22 @@ export default class extends Controller { @@ -138,17 +138,22 @@ export default class extends Controller {
console.log('[nostr_single_sign] Event signed successfully:', signed);
this.showStatus('Publishing…');
await this.publishSigned(signed);
const result = await this.publishSigned(signed);
console.log('[nostr_single_sign] Event published successfully');
// Handle redirect based on whether it's a draft or published article
if (result.isDraft) {
this.showSuccess('Draft saved successfully!');
// Stay on current page for drafts
} else {
this.showSuccess('Published successfully! Redirecting...');
// Redirect to reading list index after successful publish (only for form-based usage)
// Redirect to reading list or article page after successful publish
if (this.hasPublishButtonTarget) {
setTimeout(() => {
window.location.href = '/reading-list';
}, 1500);
}
}
} catch (e) {
console.error('[nostr_single_sign] Error during sign/publish:', e);
this.showError(e.message || 'Publish failed');
@ -178,7 +183,15 @@ export default class extends Controller { @@ -178,7 +183,15 @@ export default class extends Controller {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${res.status}`);
}
return res.json();
const result = await res.json();
// Display relay results if available
if (result.relayResults) {
this.displayRelayResults(result.relayResults);
}
return result;
}
ensureCreatedAt(evt) {
@ -188,6 +201,81 @@ export default class extends Controller { @@ -188,6 +201,81 @@ export default class extends Controller {
if (typeof evt.content !== 'string') evt.content = '';
}
displayRelayResults(relayResults) {
console.log('[nostr_single_sign] Relay results:', relayResults);
// Handle error case
if (relayResults.error) {
window.showToast(`Relay error: ${relayResults.error}`, 'warning', 6000);
return;
}
// Parse relay results from backend
if (!Array.isArray(relayResults) || relayResults.length === 0) {
window.showToast('Published to relays (status unknown)', 'info', 4000);
return;
}
// Count successes, failures, and unknowns
let successCount = 0;
let failureCount = 0;
let unknownCount = 0;
const failedRelays = [];
const unknownRelays = [];
relayResults.forEach((result) => {
const relayUrl = result.relay || 'Unknown relay';
if (result.success) {
successCount++;
} else if (result.type === 'auth') {
// AUTH responses are unknown status - not confirmed but not failed
unknownCount++;
unknownRelays.push(relayUrl);
} else {
// Other non-success responses are failures
failureCount++;
failedRelays.push(relayUrl);
}
});
// Show summary toast
if (successCount > 0 && failureCount === 0 && unknownCount === 0) {
// Perfect success
window.showToast(`✓ Published to ${successCount} relay${successCount > 1 ? 's' : ''}`, 'success', 5000);
} else if (successCount > 0 && unknownCount > 0 && failureCount === 0) {
// Success with some unknowns
window.showToast(`✓ Published to ${successCount} relay${successCount > 1 ? 's' : ''}, ${unknownCount} unknown`, 'success', 6000);
// Show details about unknowns
if (unknownRelays.length > 0) {
setTimeout(() => {
window.showToast(`Unknown status: ${unknownRelays.join(', ')}`, 'info', 8000);
}, 500);
}
} else if (successCount > 0 && failureCount > 0) {
// Mixed success and failure
const statusParts = [`${successCount} success`];
if (unknownCount > 0) statusParts.push(`${unknownCount} unknown`);
statusParts.push(`${failureCount} failed`);
window.showToast(`Published: ${statusParts.join(', ')}`, 'warning', 6000);
// Show details about failures
setTimeout(() => {
if (failedRelays.length > 0) {
window.showToast(`Failed: ${failedRelays.join(', ')}`, 'danger', 8000);
}
if (unknownRelays.length > 0) {
setTimeout(() => {
window.showToast(`Unknown: ${unknownRelays.join(', ')}`, 'info', 8000);
}, 500);
}
}, 500);
} else if (failureCount > 0 || unknownCount > 0) {
// No successes
window.showToast(`✗ Publishing uncertain (${unknownCount} unknown, ${failureCount} failed)`, 'warning', 8000);
}
}
showStatus(message) {
// Use toast system if available, otherwise fallback to status target
if (typeof window.showToast === 'function') {

90
src/Controller/ArticleController.php

@ -2,13 +2,12 @@ @@ -2,13 +2,12 @@
namespace App\Controller;
use App\Dto\AdvancedMetadata;
use App\Entity\Article;
use App\Entity\User;
use App\Enum\KindsEnum;
use App\Form\EditorType;
use App\Service\HighlightService;
use App\Service\NostrClient;
use App\Service\Nostr\NostrEventBuilder;
use App\Service\Nostr\NostrEventParser;
use App\Service\RedisCacheService;
use App\Service\RedisViewStore;
@ -25,9 +24,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; @@ -25,9 +24,6 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use App\ReadModel\RedisView\RedisReadingListView;
use App\ReadModel\RedisView\RedisBaseObject;
use App\ReadModel\RedisView\RedisArticleView;
class ArticleController extends AbstractController
{
@ -434,19 +430,46 @@ class ArticleController extends AbstractController @@ -434,19 +430,46 @@ class ArticleController extends AbstractController
$cacheKey = 'article_' . $article->getEventId();
$articlesCache->delete($cacheKey);
/** @var User $user */
$user = $this->getUser();
$relays = [];
if ($user) {
$relays = $user->getRelays();
}
// Publish to Nostr relays
$relayResults = [];
try {
$nostrClient->publishEvent($eventObj, []);
$rawResults = $nostrClient->publishEvent($eventObj, $relays);
$logger->info('Published to Nostr relays', [
'event_id' => $eventObj->getId(),
'results' => $rawResults
]);
// Transform relay results into a simpler format for frontend
$relayResults = $this->transformRelayResults($rawResults);
} catch (\Exception $e) {
// Log error but don't fail the request - article is saved locally
error_log('Failed to publish to Nostr relays: ' . $e->getMessage());
$logger->error('Failed to publish to Nostr relays', [
'error' => $e->getMessage(),
'event_id' => $eventObj->getId()
]);
$relayResults = [
'error' => $e->getMessage()
];
}
// Determine if this is a draft or published article
$isDraft = ($signedEvent['kind'] === KindsEnum::LONGFORM_DRAFT->value);
return new JsonResponse([
'success' => true,
'message' => 'Article published successfully',
'message' => $isDraft ? 'Draft saved successfully' : 'Article published successfully',
'articleId' => $article->getId(),
'slug' => $article->getSlug()
'slug' => $article->getSlug(),
'isDraft' => $isDraft,
'relayResults' => $relayResults
]);
} catch (\Exception $e) {
@ -456,6 +479,55 @@ class ArticleController extends AbstractController @@ -456,6 +479,55 @@ class ArticleController extends AbstractController
}
}
/**
* Transform relay response objects into a simple array format for frontend
*/
private function transformRelayResults(array $rawResults): array
{
$results = [];
foreach ($rawResults as $relayUrl => $response) {
$result = [
'relay' => $relayUrl,
'success' => false,
'type' => 'unknown',
'message' => ''
];
// Check if it's a RelayResponse object with accessible properties
if (is_object($response)) {
// RelayResponseOk - indicates successful publish
if (isset($response->type) && $response->type === 'OK') {
$result['success'] = true;
$result['type'] = 'ok';
$result['message'] = $response->message ?? '';
}
// RelayResponseAuth - relay requires auth (not necessarily a failure)
elseif (isset($response->type) && $response->type === 'AUTH') {
$result['success'] = false; // Not confirmed published
$result['type'] = 'auth';
$result['message'] = 'Authentication required';
}
// RelayResponseNotice - informational message
elseif (isset($response->type) && $response->type === 'NOTICE') {
$result['success'] = false;
$result['type'] = 'notice';
$result['message'] = $response->message ?? '';
}
// Check isSuccess property if available
elseif (isset($response->isSuccess)) {
$result['success'] = (bool)$response->isSuccess;
$result['type'] = $response->type ?? 'unknown';
$result['message'] = $response->message ?? '';
}
}
$results[] = $result;
}
return $results;
}
private function validateNostrEvent(array $event): void
{
$requiredFields = ['id', 'pubkey', 'created_at', 'kind', 'tags', 'content', 'sig'];

7
src/Controller/AuthorController.php

@ -282,8 +282,13 @@ class AuthorController extends AbstractController @@ -282,8 +282,13 @@ class AuthorController extends AbstractController
$latest = isset($articles[0]['article']['publishedAt'])
? strtotime($articles[0]['article']['publishedAt'])
: time();
} else {
} else if ($articles[0] instanceof Article) {
// Article entity
$latest = $articles[0]->getCreatedAt()->getTimestamp();
} else {
// Fallback
// Something went wrong upstream, use current time
$latest = time();
}
// Dispatch async message to fetch new articles since latest + 1
$messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, $latest + 1));

4
src/Service/NostrClient.php

@ -118,7 +118,7 @@ class NostrClient @@ -118,7 +118,7 @@ class NostrClient
$authorRelays = array_filter($authorRelays, function ($relay) {
return str_starts_with($relay, 'wss:') && !str_contains($relay, 'localhost');
});
return array_slice($authorRelays, 0, $limit);
return $limit != 0 ? array_slice($authorRelays, 0, $limit) : $authorRelays;
}
return $reputableAuthorRelays;
@ -167,7 +167,7 @@ class NostrClient @@ -167,7 +167,7 @@ class NostrClient
// If no relays, fetch relays for user then post to those
if (empty($relays)) {
$key = new Key();
$relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 5);
$relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 0);
} else {
// Ensure local relay is included when publishing
$relays = $this->relayPool->ensureLocalRelayInList($relays);

Loading…
Cancel
Save