mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: stories (#2172)
* feat: integrate Stories widget with backend endpoint
Add stories endpoint plumbing for HTTP and Wails, wire the Home Stories card
to fetch from /api/alby/stories, and keep it first in the right column.
Made-with: Cursor
* feat(home): story modal CTAs and preview fallback
- Add contextual actions in the story dialog (update hub with version,
open Alby Go in-app, install extension) keyed by kind or title
- Use preview stories when the stories API request fails
- Pass hub version from useInfo into the update link
Made-with: Cursor
* feat(stories): polish modal, drop preview fallback
- Widen modal and put video edge-to-edge with overlay close button
- Drop verbose header and 'Watch on YouTube' button
- Remove previewStories fallback so widget hides until upstream API ships
- Tighten title line-height
* feat(stories): render cta from API instead of mapping by kind
Move CTA copy and URLs into the API response. Hub renders story.cta
directly, so adding new story types no longer requires a hub release.
* chore(csp): allow cdn.getalby-assets.com in img-src
* feat(stories): bump avatar size and add ring gap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): post-review cleanups
- Use react-router Link for in-tab CTA instead of plain <a>.
- Drop redundant www.youtube.com from frame-src (embeds always go through nocookie).
- Tighten stories endpoint status check from >= 300 to >= 400.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): address CodeRabbit feedback
- Switch StoriesWidget to useSWR + swrFetcher (project convention).
- Guard story iframe with isYouTubeUrl so non-YouTube urls never embed.
- Wrap GetStories errors with fmt.Errorf("...: %w", err).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): drop isYouTubeUrl guard
Stories are curated and always YouTube; the runtime check was
redundant. CSP frame-src still constrains the iframe source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): drop getYouTubeEmbedUrl, embed videoUrl as-is
The Alby API now sends canonical youtube-nocookie embed URLs with
autoplay/rel query strings (getAlby/getalby.com#2568), so the
runtime normalization is no longer needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): use w-16 instead of arbitrary w-[73px]
Match the avatar's size token; no magic numbers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): take videoId from API and assemble embed url locally
Pairs with getAlby/getalby.com#2568. The API now sends just the
YouTube videoId; the hub composes the canonical embed URL so the
domain/query-string format stays in one place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): treat 3xx as non-success, matching file convention
The other status checks in alby_oauth_service.go all use >= 300;
align GetStories so redirects don't slip through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): move viewed-storage key to constants, widen story button
Address review feedback:
- Centralize the localStorage key for viewed stories in localStorageKeys
alongside the other keys.
- Widen the story button from w-16 to w-20 so "Alby Extension" fits on
one line and matches the other titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): bump story button to w-24 so titles fit one line
w-20 still wrapped "Alby Extension"; w-24 fits all current titles
without truncation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(stories): bump story button to w-24 so titles fit one line"
This reverts commit 0b47438f50.
* chore(stories): split title words onto separate lines
Reserve two lines for every story title so avatars align regardless of
title length.
* chore(stories): align homeStoriesViewed key with sibling pattern
* chore(stories): fit titles on one line
* chore(stories): widen story button to w-21 for one-line titles
---------
Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81b1b2f695
commit
23dccc6c6f
12 changed files with 373 additions and 2 deletions
|
|
@ -1369,6 +1369,52 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetStories(ctx context.Context) ([]Story, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
url := fmt.Sprintf("%s/stories", albyInternalAPIURL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request to stories endpoint")
|
||||
return nil, fmt.Errorf("create stories request: %w", err)
|
||||
}
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch stories from API")
|
||||
return nil, fmt.Errorf("fetch stories: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to read response body")
|
||||
return nil, fmt.Errorf("read stories response body: %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("stories endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("stories endpoint returned %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var stories []Story
|
||||
if err := json.Unmarshal(body, &stories); err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"error": err,
|
||||
}).Error("Failed to decode stories API response")
|
||||
return nil, fmt.Errorf("decode stories response: %w", err)
|
||||
}
|
||||
|
||||
return stories, nil
|
||||
}
|
||||
|
||||
func setDefaultRequestHeaders(req *http.Request) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type AlbyOAuthService interface {
|
|||
RemoveOAuthAccessToken() error
|
||||
CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error)
|
||||
DeleteLightningAddress(ctx context.Context, address string) error
|
||||
GetStories(ctx context.Context) ([]Story, error)
|
||||
}
|
||||
|
||||
type CreateLightningAddressResponse struct {
|
||||
|
|
@ -153,6 +154,20 @@ type ErrorResponse struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StoryCta struct {
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
OpenInNewTab bool `json:"openInNewTab"`
|
||||
}
|
||||
|
||||
type Story struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Avatar string `json:"avatar"`
|
||||
VideoID string `json:"videoId,omitempty"`
|
||||
Cta *StoryCta `json:"cta,omitempty"`
|
||||
}
|
||||
|
||||
type LSPChannelPaymentBolt11 struct {
|
||||
Invoice string `json:"invoice"`
|
||||
FeeTotalSat string `json:"fee_total_sat"`
|
||||
|
|
|
|||
|
|
@ -756,6 +756,10 @@ func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPe
|
|||
return api.albySvc.GetChannelPeerSuggestions(ctx)
|
||||
}
|
||||
|
||||
func (api *api) GetStories(ctx context.Context) ([]alby.Story, error) {
|
||||
return api.albyOAuthSvc.GetStories(ctx)
|
||||
}
|
||||
|
||||
func (api *api) GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error) {
|
||||
return api.albyOAuthSvc.GetLSPChannelOffer(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type API interface {
|
|||
DeleteLightningAddress(ctx context.Context, appId uint) error
|
||||
ListChannels(ctx context.Context) ([]Channel, error)
|
||||
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
|
||||
GetStories(ctx context.Context) ([]alby.Story, error)
|
||||
GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error)
|
||||
ResetRouter(key string) error
|
||||
ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error
|
||||
|
|
|
|||
251
frontend/src/components/home/widgets/StoriesWidget.tsx
Normal file
251
frontend/src/components/home/widgets/StoriesWidget.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { XIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
import useSWR from "swr";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "src/components/ui/dialog";
|
||||
import { localStorageKeys } from "src/constants";
|
||||
import { cn } from "src/lib/utils";
|
||||
import { swrFetcher } from "src/utils/swr";
|
||||
|
||||
type StoryCta = {
|
||||
label: string;
|
||||
url: string;
|
||||
openInNewTab: boolean;
|
||||
};
|
||||
|
||||
type Story = {
|
||||
id: string;
|
||||
title: string;
|
||||
avatar: string;
|
||||
videoId?: string;
|
||||
cta?: StoryCta;
|
||||
};
|
||||
|
||||
type StoryApiResponse = {
|
||||
id: number;
|
||||
title: string;
|
||||
avatar: string;
|
||||
videoId?: string;
|
||||
cta?: StoryCta;
|
||||
};
|
||||
|
||||
function youTubeEmbedUrl(videoId: string) {
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`;
|
||||
}
|
||||
|
||||
function loadViewedStoryIds(): Set<string> {
|
||||
try {
|
||||
const raw = localStorage.getItem(localStorageKeys.homeStoriesViewed);
|
||||
if (!raw) {
|
||||
return new Set();
|
||||
}
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(parsed.filter((id): id is string => typeof id === "string"));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function persistViewedStoryIds(ids: Set<string>) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
localStorageKeys.homeStoriesViewed,
|
||||
JSON.stringify([...ids])
|
||||
);
|
||||
} catch {
|
||||
// ignore quota / private mode
|
||||
}
|
||||
}
|
||||
|
||||
function StoryAvatar({ story, viewed }: { story: Story; viewed: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative box-border flex size-16 shrink-0 items-center justify-center rounded-full border-2 p-0.5",
|
||||
viewed ? "border-accent" : "border-primary"
|
||||
)}
|
||||
>
|
||||
<div className="relative flex size-full items-center justify-center overflow-hidden rounded-full bg-white dark:bg-muted">
|
||||
<img
|
||||
src={story.avatar}
|
||||
alt={`${story.title} story`}
|
||||
className="size-full rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StoriesWidget() {
|
||||
const { data, error, isLoading } = useSWR<StoryApiResponse[]>(
|
||||
"/api/alby/stories",
|
||||
swrFetcher
|
||||
);
|
||||
const [activeStory, setActiveStory] = React.useState<Story | null>(null);
|
||||
const [viewedIds, setViewedIds] =
|
||||
React.useState<Set<string>>(loadViewedStoryIds);
|
||||
|
||||
const stories = React.useMemo<Story[]>(
|
||||
() =>
|
||||
error || !data
|
||||
? []
|
||||
: data.map((story) => ({
|
||||
id: String(story.id),
|
||||
title: story.title,
|
||||
avatar: story.avatar,
|
||||
videoId: story.videoId,
|
||||
cta: story.cta,
|
||||
})),
|
||||
[data, error]
|
||||
);
|
||||
|
||||
const markStoryViewed = React.useCallback((storyId: string) => {
|
||||
setViewedIds((prev) => {
|
||||
if (prev.has(storyId)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Set(prev);
|
||||
next.add(storyId);
|
||||
persistViewedStoryIds(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!isLoading && stories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="overflow-hidden rounded-[14px] shadow-none">
|
||||
<CardHeader className="px-6 pb-0">
|
||||
<CardTitle className="text-base font-semibold">Stories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 py-0">
|
||||
<div className="flex gap-3 overflow-x-auto px-6 pb-1">
|
||||
{isLoading && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Loading stories...
|
||||
</span>
|
||||
)}
|
||||
{!isLoading &&
|
||||
stories.map((story) => {
|
||||
const viewed = viewedIds.has(story.id);
|
||||
return (
|
||||
<button
|
||||
key={story.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
markStoryViewed(story.id);
|
||||
setActiveStory(story);
|
||||
}}
|
||||
className="flex w-21 shrink-0 flex-col items-center gap-2 text-center"
|
||||
>
|
||||
<StoryAvatar story={story} viewed={viewed} />
|
||||
<span
|
||||
className={cn(
|
||||
"w-full truncate text-xs leading-tight",
|
||||
viewed
|
||||
? "font-medium text-muted-foreground"
|
||||
: "font-semibold text-foreground"
|
||||
)}
|
||||
>
|
||||
{story.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
open={!!activeStory}
|
||||
onOpenChange={(open) => !open && setActiveStory(null)}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="w-[95vw] max-w-[min(95vw,calc((90vh-80px)*16/9))] sm:max-w-[min(95vw,calc((90vh-80px)*16/9))] max-h-[90vh] overflow-hidden border-0 bg-zinc-950 p-0 text-white sm:rounded-2xl"
|
||||
>
|
||||
{activeStory && (
|
||||
<div className="flex flex-col">
|
||||
<DialogTitle className="sr-only">{activeStory.title}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Watch the latest update
|
||||
</DialogDescription>
|
||||
|
||||
{activeStory.videoId && (
|
||||
<div className="relative aspect-video w-full overflow-hidden rounded-t-2xl bg-black [transform:translateZ(0)]">
|
||||
<iframe
|
||||
className="absolute inset-0 size-full"
|
||||
src={youTubeEmbedUrl(activeStory.videoId)}
|
||||
title={activeStory.title}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-3 top-3 z-10 rounded-full bg-black/60 text-white backdrop-blur hover:bg-black/80 hover:text-white"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
<span className="sr-only">Close story</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStory.videoId && (
|
||||
<div className="flex items-center justify-between gap-3 px-6 py-4">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-base font-semibold text-white">
|
||||
{activeStory.title}
|
||||
</div>
|
||||
</div>
|
||||
{activeStory.cta && (
|
||||
<div className="flex items-center gap-2">
|
||||
{activeStory.cta.openInNewTab ? (
|
||||
<ExternalLink
|
||||
to={activeStory.cta.url}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{activeStory.cta.label}
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<Link
|
||||
to={activeStory.cta.url}
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{activeStory.cta.label}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export const localStorageKeys = {
|
|||
supportAlbySidebarHintHiddenUntil: "supportAlbySidebarHintHiddenUntil",
|
||||
aiHeroDismissed: "aiHeroDismissed",
|
||||
cardsHeroDismissed: "cardsHeroDismissed",
|
||||
homeStoriesViewed: "homeStoriesViewed",
|
||||
};
|
||||
|
||||
export const ONCHAIN_DUST_SATS = 1000;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { LightningMessageboardWidget } from "src/components/home/widgets/Lightni
|
|||
import { NewArrivalsWidget } from "src/components/home/widgets/NewArrivalsWidget";
|
||||
import { NodeStatusWidget } from "src/components/home/widgets/NodeStatusWidget";
|
||||
import { OnchainFeesWidget } from "src/components/home/widgets/OnchainFeesWidget";
|
||||
import { StoriesWidget } from "src/components/home/widgets/StoriesWidget";
|
||||
import { SupportAlbyWidget } from "src/components/home/widgets/SupportAlbyWidget";
|
||||
import { WhatsNewWidget } from "src/components/home/widgets/WhatsNewWidget";
|
||||
import { SearchInput } from "src/components/ui/search-input";
|
||||
|
|
@ -44,6 +45,7 @@ function Home() {
|
|||
/>
|
||||
<div className="columns-1 lg:columns-2 gap-3 *:mb-3 *:break-inside-avoid">
|
||||
<OnboardingChecklist />
|
||||
<StoriesWidget />
|
||||
<WhatsNewWidget />
|
||||
<LatestUsedAppsWidget />
|
||||
<NewArrivalsWidget />
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const insertDevCSPPlugin: Plugin = {
|
|||
"<head>",
|
||||
`<head>
|
||||
<!-- DEV-ONLY CSP - when making changes here, also update the CSP header in http_service.go (without the nonce!) -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com" />`
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com https://www.youtube-nocookie.com" />`
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(readOnlyApiGroup *echo.
|
|||
e.GET("/api/alby/info", albyHttpSvc.albyInfoHandler)
|
||||
e.GET("/api/alby/rates/:currency", albyHttpSvc.albyBitcoinRateHandler)
|
||||
e.GET("/api/alby/currencies", albyHttpSvc.albyCurrenciesHandler)
|
||||
e.GET("/api/alby/stories", albyHttpSvc.albyStoriesHandler)
|
||||
readOnlyApiGroup.GET("/alby/me", albyHttpSvc.albyMeHandler)
|
||||
fullAccessApiGroup.POST("/alby/link-account", albyHttpSvc.albyLinkAccountHandler)
|
||||
fullAccessApiGroup.POST("/alby/auto-channel", albyHttpSvc.autoChannelHandler)
|
||||
|
|
@ -109,6 +110,17 @@ func (albyHttpSvc *AlbyHttpService) albyCurrenciesHandler(c echo.Context) error
|
|||
return c.JSON(http.StatusOK, currencies)
|
||||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) albyStoriesHandler(c echo.Context) error {
|
||||
stories, err := albyHttpSvc.albyOAuthSvc.GetStories(c.Request().Context())
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get stories")
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to get stories: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusOK, stories)
|
||||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error {
|
||||
code := c.QueryParam("code")
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
|
||||
ContentTypeNosniff: "nosniff",
|
||||
XFrameOptions: "DENY",
|
||||
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com",
|
||||
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com https://www.youtube-nocookie.com",
|
||||
ReferrerPolicy: "no-referrer",
|
||||
}))
|
||||
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
|
|
|
|||
|
|
@ -655,6 +655,34 @@ func (_c *MockAlbyOAuthService_GetMe_Call) RunAndReturn(run func(ctx context.Con
|
|||
return _c
|
||||
}
|
||||
|
||||
// GetStories provides a mock function for the type MockAlbyOAuthService
|
||||
func (_mock *MockAlbyOAuthService) GetStories(ctx context.Context) ([]alby.Story, error) {
|
||||
ret := _mock.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetStories")
|
||||
}
|
||||
|
||||
var r0 []alby.Story
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context) ([]alby.Story, error)); ok {
|
||||
return returnFunc(ctx)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context) []alby.Story); ok {
|
||||
r0 = returnFunc(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]alby.Story)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = returnFunc(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserIdentifier provides a mock function for the type MockAlbyOAuthService
|
||||
func (_mock *MockAlbyOAuthService) GetUserIdentifier() (string, error) {
|
||||
ret := _mock.Called()
|
||||
|
|
|
|||
|
|
@ -465,6 +465,17 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: currencies, Error: ""}
|
||||
case "/api/alby/stories":
|
||||
stories, err := app.api.GetStories(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
"method": method,
|
||||
"body": body,
|
||||
}).WithError(err).Error("Failed to get stories")
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: stories, Error: ""}
|
||||
case "/api/apps":
|
||||
switch method {
|
||||
case "POST":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue