Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notify founder when there is a new investment #236

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
38 changes: 22 additions & 16 deletions src/Angor/Client/Components/FounderProjectItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@
@using Angor.Client.Models
@using Angor.Client.Storage
@using System.Text.RegularExpressions

@using Blockcore.NBitcoin


@inject IRelayService RelayService;
@inject IClientStorage Storage;
@inject IHtmlStripperService HtmlStripperService;



<div class="col d-flex align-items-stretch">
<div class="card mt-4 w-100 project-card">
<a class="d-block">

<div class="banner-container">
<img class="banner-image" src="@(FounderProject?.Metadata?.Banner ?? "/assets/img/no-image.jpg")" alt="@(@FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';" />
<img class="banner-image" src="@(FounderProject?.Metadata?.Banner ?? "/assets/img/no-image.jpg")" alt="@(@FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';"/>
<div class="profile-container">
<img class="profile-image" src="@(FounderProject?.Metadata?.Picture ?? "/assets/img/no-image.jpg")" alt="@(FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';" />
<img class="profile-image" src="@(FounderProject?.Metadata?.Picture ?? "/assets/img/no-image.jpg")" alt="@(FounderProject?.Metadata?.Banner != null ? "" : "no-image")" onerror="this.onerror=null; this.src='/assets/img/no-image.jpg';"/>
</div>
</div>

</a>

<div class="card-body pb-0">



<div class="d-flex align-items-center mb-4">
<span class="user-select-none">
<Icon IconName="view" Height="24" Width="24"></Icon>
Expand All @@ -39,12 +34,25 @@
</div>
<p class="mb-0 line-clamp-3">@(ConvertToMarkupString(FounderProject.Metadata.About))</p>



<!-- Add Investment Stats Section -->
<div class="investment-stats mt-3">
<div class="d-flex align-items-center mb-2">
<Icon IconName="users"></Icon>
<p class="mb-0 ms-2">
<b>Total Investors:</b> @FounderProject.Stats.InvestorCount
</p>
</div>
<div class="d-flex align-items-center">
<Icon IconName="calculator"></Icon>
<p class="mb-0 ms-2">
<b>Total Raised:</b> @Money.Satoshis(FounderProject.Stats.AmountInvested).ToUnit(MoneyUnit.BTC) BTC
</p>
</div>
</div>
</div>

<div class="card-footer pt-0">
<hr class="horizontal light mt-3">

<a role="button" class="d-flex align-items-center btn btn-border w-100-m" href=@($"/view/{FounderProject.ProjectInfo.ProjectIdentifier}")>
<span class="user-select-none">
<Icon IconName="view-project" Height="24" Width="24"></Icon>
Expand All @@ -69,16 +77,14 @@
</div>
</a>
}

</div>
</div>
</div>


@code {

[Parameter]
public FounderProject FounderProject { get; set; }
[Parameter] public FounderProject FounderProject { get; set; }

public bool InvestmentRequests { get; set; }

Expand All @@ -95,11 +101,11 @@
});
}



public MarkupString ConvertToMarkupString(string input)
{
string sanitizedInput = HtmlStripperService.StripHtmlTags(input);
return new MarkupString(sanitizedInput);
}

}
2 changes: 2 additions & 0 deletions src/Angor/Client/Models/FounderProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public bool NostrApplicationSpecificDataCreated()
{
return !string.IsNullOrEmpty(ProjectInfoEventId);
}
public ProjectStats Stats { get; set; } = new ProjectStats();

}
141 changes: 100 additions & 41 deletions src/Angor/Client/Pages/Founder.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@using Angor.Client.Storage
@using Angor.Shared.Models
@using Angor.Shared.Services
@using Blockcore.NBitcoin
@using Nostr.Client.Messages

@inject NavigationManager NavigationManager
Expand Down Expand Up @@ -38,7 +39,8 @@
<button
class="btn btn-border"
@onclick="NavigateToCreateProject"
disabled="@(scanningForProjects || (founderProjects.Count >= 14) ? true : null)"> <i>
disabled="@(scanningForProjects || (founderProjects.Count >= 14) ? true : null)">
<i>
<Icon IconName="add"></Icon>
</i>
<span class="nav-link-text ms-1">
Expand Down Expand Up @@ -108,6 +110,7 @@
if (hasWallet)
{
founderProjects = storage.GetFounderProjects().Where(_ => !string.IsNullOrEmpty(_.CreationTransactionId)).ToList();
await ScanForNewInvestmentsAsync();
}
}

Expand All @@ -125,7 +128,7 @@

var indexerProject = await _IndexerService.GetProjectByIdAsync(key.ProjectIdentifier);

if (indexerProject != null) //TODO we need to talk about supporting projects that are created with gaps
if (indexerProject != null) // TODO: Support projects with gaps
founderProjectsToLookup.Add(key.NostrPubKey, indexerProject);
}

Expand All @@ -138,48 +141,62 @@
RelayService.RequestProjectCreateEventsByPubKey(
e =>
{
switch (e)
// Run the async logic in a fire-and-forget task
_ = Task.Run(async () =>
{
case { Kind: NostrKind.Metadata }:
var nostrMetadata = serializer.Deserialize<ProjectMetadata>(e.Content);
var existingProject = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (existingProject != null)
{
existingProject.Metadata ??= nostrMetadata;
}
else
{
var founderProject = CreateFounderProject(founderProjectsToLookup, e);
founderProject.Metadata = nostrMetadata;
founderProjects.Add(founderProject);
}

break;

case { Kind: NostrKind.ApplicationSpecificData }:

if (e.Id != founderProjectsToLookup[e.Pubkey].NostrEventId)
return;

var projectInfo = serializer.Deserialize<ProjectInfo>(e.Content);
var project = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (project != null)
{
if (!string.IsNullOrEmpty(project.ProjectInfo.ProjectIdentifier))
switch (e)
{
case { Kind: NostrKind.Metadata }:
var nostrMetadata = serializer.Deserialize<ProjectMetadata>(e.Content);
var existingProject = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

if (existingProject != null)
{
existingProject.Metadata ??= nostrMetadata;
}
else
{
var founderProject = CreateFounderProject(founderProjectsToLookup, e);
founderProject.Metadata = nostrMetadata;

// Initialize stats for the new project
founderProject.Stats = await _IndexerService.GetProjectStatsAsync(founderProject.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();

founderProjects.Add(founderProject);
}

break;

case { Kind: NostrKind.ApplicationSpecificData }:
if (e.Id != founderProjectsToLookup[e.Pubkey].NostrEventId)
return;

project.ProjectInfo = projectInfo;
}
else
{
project ??= CreateFounderProject(founderProjectsToLookup, e, projectInfo);
founderProjects.Add(project);
}
var projectInfo = serializer.Deserialize<ProjectInfo>(e.Content);
var project = founderProjects.FirstOrDefault(_ => _.ProjectInfo.NostrPubKey == e.Pubkey);

break;
}
if (project != null)
{
if (!string.IsNullOrEmpty(project.ProjectInfo.ProjectIdentifier))
return;

project.ProjectInfo = projectInfo;

// Update stats for the existing project
project.Stats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();
}
else
{
project = CreateFounderProject(founderProjectsToLookup, e, projectInfo);

// Initialize stats for the new project
project.Stats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier) ?? new ProjectStats();

founderProjects.Add(project);
}

break;
}
});
},
() =>
{
Expand All @@ -204,7 +221,8 @@
founderProjectsToLookup.Keys.ToArray());
}

private FounderProject CreateFounderProject(Dictionary<string, ProjectIndexerData> founderProjectsToLookup,

private FounderProject CreateFounderProject(Dictionary<string, ProjectIndexerData> founderProjectsToLookup,
NostrEvent e, ProjectInfo? projectInfo = null)
{
var keys = _walletStorage.GetFounderKeys();
Expand All @@ -228,6 +246,7 @@

NavigationManager.NavigateTo("/create");
}

private string GetCreateButtonTooltip()
{
if (founderProjects.Count >= 15)
Expand All @@ -237,5 +256,45 @@
return "Create a new project.";
}

private async Task ScanForNewInvestmentsAsync()
{
try
{
foreach (var project in founderProjects)
{
if (project?.ProjectInfo == null)
continue;

// Fetch current stats from the IndexerService
var currentStats = await _IndexerService.GetProjectStatsAsync(project.ProjectInfo.ProjectIdentifier);

if (currentStats == null)
continue;

// Check for new investments
if (currentStats.InvestorCount > project.Stats.InvestorCount ||
currentStats.AmountInvested > project.Stats.AmountInvested)
{
// Update project stats
project.Stats.InvestorCount = (int)currentStats.InvestorCount;
project.Stats.AmountInvested = currentStats.AmountInvested;

// Notify the founder
notificationComponent.ShowNotificationMessage(
$"New investment detected in project '{project.ProjectInfo.ProjectIdentifier}': {currentStats.InvestorCount} investors, {Money.Satoshis(currentStats.AmountInvested).ToUnit(MoneyUnit.BTC)} BTC raised.", 5);

// Save updated project to storage
storage.UpdateFounderProject(project);
}
}

StateHasChanged();
}
catch (Exception ex)
{
Console.WriteLine($"Error scanning for investments: {ex.Message}");
}
}


}
17 changes: 17 additions & 0 deletions src/Angor/Client/Storage/ClientStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Angor.Client.Storage;
public class ClientStorage : IClientStorage, INetworkStorage
{
private const string CurrencyDisplaySettingKey = "currencyDisplaySetting";
private const string InvestmentStatsKeyPattern = "project:{0}:stats";


private const string utxoKey = "utxo:{0}";
private readonly ISyncLocalStorageService _storage;
Expand Down Expand Up @@ -222,6 +224,21 @@ public void DeleteSignatures()

_storage.RemoveItem("recovery-signatures");
}

public void SaveInvestmentStats(string projectId, ProjectStats stats)
{
_storage.SetItem(string.Format(InvestmentStatsKeyPattern, projectId), stats);
}

public void ClearInvestmentStats(string projectId)
{
_storage.RemoveItem(string.Format(InvestmentStatsKeyPattern, projectId));
}

public ProjectStats? GetInvestmentStats(string projectIdentifier)
{
var projects = GetFounderProjects();
return projects.FirstOrDefault(p => p.ProjectInfo.ProjectIdentifier == projectIdentifier)?.Stats;
}

}
4 changes: 4 additions & 0 deletions src/Angor/Client/Storage/IClientStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ public interface IClientStorage

string GetCurrencyDisplaySetting();
void SetCurrencyDisplaySetting(string setting);

void SaveInvestmentStats(string projectId, ProjectStats stats);
ProjectStats? GetInvestmentStats(string projectId);
void ClearInvestmentStats(string projectId);
}
}
Loading