* Copyright 2019-2025, Andrew Lindesay <apl@lindesay.co.nz>.
* Copyright 2024 Haiku, Inc. All rights reserved.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "UserUsageConditionsWindow.h"
#include <Button.h>
#include <Catalog.h>
#include <Font.h>
#include <LayoutBuilder.h>
#include <ScrollView.h>
#include <StringFormat.h>
#include <StringView.h>
#include "AppUtils.h"
#include "BarberPole.h"
#include "HaikuDepotConstants.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "MarkupTextView.h"
#include "Model.h"
#include "ServerHelper.h"
#include "TextView.h"
#include "UserUsageConditions.h"
#include "WebAppInterface.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "UserUsageConditions"
#define PLACEHOLDER_TEXT "..."
#define INTRODUCTION_TEXT_LATEST \
"HaikuDepot communicates with a " \
"server component called HaikuDepotServer. These are the latest " \
"usage conditions for use of the HaikuDepotServer service."
#define INTRODUCTION_TEXT_USER \
"HaikuDepot communicates with a " \
"server component called HaikuDepotServer. These are the usage " \
"conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\
"in relation to the use of the HaikuDepotServer service."
#define KEY_USER_USAGE_CONDITIONS "userUsageConditions"
#define KEY_USER_DETAIL "userDetail"
introduction.
*/
#define LINES_INTRODUCTION_TEXT 2
UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
UserUsageConditions& userUsageConditions)
:
BWindow(BRect(), B_TRANSLATE("Usage conditions"), B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS | B_NOT_ZOOMABLE),
fMode(FIXED),
fModel(model),
fIntroductionTextView(NULL),
fWorkerThread(-1)
{
_InitUiControls();
font_height fontHeight;
be_plain_font->GetHeight(&fontHeight);
const float lineHeight = fontHeight.ascent + fontHeight.descent;
BScrollView* scrollView
= new BScrollView("copy scroll view", fCopyView, 0, false, true, B_PLAIN_BORDER);
scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), new BMessage(B_QUIT_REQUESTED));
BLayoutBuilder::Group<>(this, B_VERTICAL)
.SetInsets(B_USE_WINDOW_INSETS)
.Add(fVersionStringView, 1)
.Add(scrollView, 97)
.Add(fAgeNoteStringView, 1)
.AddGroup(B_HORIZONTAL, 1)
.AddGlue()
.Add(okButton)
.End()
.End();
GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
ResizeToPreferred();
CenterOnScreen();
UserDetail userDetail;
_DisplayData(userDetail, userUsageConditions);
}
UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
UserUsageConditionsSelectionMode mode)
:
BWindow(BRect(), B_TRANSLATE("Usage conditions"), B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS | B_NOT_ZOOMABLE),
fMode(mode),
fModel(model),
fWorkerThread(-1)
{
_InitUiControls();
font_height fontHeight;
be_plain_font->GetHeight(&fontHeight);
const float lineHeight = fontHeight.ascent + fontHeight.descent;
fWorkerIndicator = new BarberPole("fetch data worker indicator");
BSize workerIndicatorSize;
workerIndicatorSize.SetHeight(lineHeight);
fWorkerIndicator->SetExplicitSize(workerIndicatorSize);
fIntroductionTextView = new TextView("introduction text view");
UserDetail userDetail;
fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail));
BScrollView* scrollView
= new BScrollView("copy scroll view", fCopyView, 0, false, true, B_PLAIN_BORDER);
scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), new BMessage(B_QUIT_REQUESTED));
BLayoutBuilder::Group<>(this, B_VERTICAL)
.SetInsets(B_USE_WINDOW_INSETS)
.Add(fIntroductionTextView, 1)
.AddGlue()
.Add(fVersionStringView, 1)
.Add(scrollView, 95)
.Add(fAgeNoteStringView, 1)
.AddGroup(B_HORIZONTAL, 1)
.AddGlue()
.Add(okButton)
.End()
.Add(fWorkerIndicator, 1)
.End();
GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
ResizeToPreferred();
CenterOnScreen();
_FetchData();
}
UserUsageConditionsWindow::~UserUsageConditionsWindow()
{
}
a given mode of viewing.
*/
void
UserUsageConditionsWindow::_InitUiControls()
{
fCopyView = new MarkupTextView("copy view");
fCopyView->SetViewUIColor(B_NO_COLOR);
fCopyView->SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
fCopyView->SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
fCopyView->SetInsets(8.0f);
fAgeNoteStringView = new BStringView("age note string view", PLACEHOLDER_TEXT);
fAgeNoteStringView->AdoptSystemColors();
BFont versionFont(be_plain_font);
versionFont.SetSize(versionFont.Size() * 0.75f);
fVersionStringView = new BStringView("version string view", PLACEHOLDER_TEXT);
fVersionStringView->AdoptSystemColors();
fVersionStringView->SetFont(&versionFont);
fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
}
void
UserUsageConditionsWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_USER_USAGE_CONDITIONS_DATA:
{
BMessage userDetailMessage;
BMessage userUsageConditionsMessage;
message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
message->FindMessage(KEY_USER_USAGE_CONDITIONS, &userUsageConditionsMessage);
UserDetail userDetail(&userDetailMessage);
UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
_DisplayData(userDetail, userUsageConditions);
fWorkerIndicator->Stop();
break;
}
default:
BWindow::MessageReceived(message);
break;
}
}
bool
UserUsageConditionsWindow::QuitRequested()
{
if (fWorkerThread == -1)
return true;
HDINFO("unable to quit when the user usage "
"conditions window is still fetching data");
return false;
}
processing to obtain the user usage conditions data. It will take
responsibility for coordinating the creation of the thread and starting the
thread etc...
*/
void
UserUsageConditionsWindow::_FetchData()
{
if (-1 != fWorkerThread)
debugger("illegal state - attempt to fetch, but fetch in progress");
thread_id thread = spawn_thread(&_FetchDataThreadEntry, "Fetch usage conditions data",
B_NORMAL_PRIORITY, this);
if (thread >= 0) {
fWorkerIndicator->Start();
_SetWorkerThread(thread);
resume_thread(fWorkerThread);
} else {
debugger("unable to start a thread to fetch the user usage "
"conditions.");
}
}
the entry-point for the background processing to obtain the user usage
conditions.
*/
int32
UserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
{
UserUsageConditionsWindow* win = reinterpret_cast<UserUsageConditionsWindow*>(data);
win->_FetchDataPerform();
return 0;
}
conditions.
*/
void
UserUsageConditionsWindow::_FetchDataPerform()
{
UserDetail userDetail;
UserUsageConditions conditions;
WebAppInterfaceRef interface = fModel.WebApp();
BString code;
status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code);
if (status == B_OK) {
if (fMode == USER && code.IsEmpty()) {
BString message = B_TRANSLATE("The user '%Nickname%' has not agreed to any usage "
"conditions.");
message.ReplaceAll("%Nickname%", userDetail.Nickname());
AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"), message);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
status = B_BAD_DATA;
}
} else {
_NotifyFetchProblem();
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
}
if (status == B_OK) {
if (interface->RetrieveUserUsageConditions(code, conditions) == B_OK) {
BMessage userUsageConditionsMessage;
BMessage userDetailMessage;
conditions.Archive(&userUsageConditionsMessage, true);
userDetail.Archive(&userDetailMessage, true);
BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS, &userUsageConditionsMessage);
dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage);
BMessenger(this).SendMessage(&dataMessage);
} else {
_NotifyFetchProblem();
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
}
}
_SetWorkerThread(-1);
}
status_t
UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform(UserDetail& userDetail,
BString& code)
{
switch (fMode) {
case LATEST:
code.SetTo("");
return B_OK;
case USER:
return _FetchUserUsageConditionsCodeForUserPerform(userDetail, code);
default:
debugger("unhanded mode");
return B_ERROR;
}
}
status_t
UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform(UserDetail& userDetail,
BString& code)
{
WebAppInterfaceRef interface = fModel.WebApp();
if (interface->Nickname().IsEmpty()) {
debugger("attempt to get user details for the current user, but"
" there is no current user");
}
BMessage responseEnvelopeMessage;
status_t result = interface->RetrieveCurrentUserDetail(responseEnvelopeMessage);
if (result == B_OK) {
switch (WebAppInterface::ErrorCodeFromResponse(responseEnvelopeMessage)) {
case ERROR_CODE_NONE:
result = WebAppInterface::UnpackUserDetail(responseEnvelopeMessage, userDetail);
break;
default:
ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage);
result = B_ERROR;
break;
}
} else {
HDERROR("an error has arisen communicating with the"
" server to obtain data for a user's user usage conditions"
" [%s]",
strerror(result));
ServerHelper::NotifyTransportError(result);
}
if (result == B_OK) {
BString userUsageConditionsCode = userDetail.Agreement().Code();
HDDEBUG("the user [%s] has agreed to uuc [%s]", interface->Nickname().String(),
userUsageConditionsCode.String());
code.SetTo(userUsageConditionsCode);
} else {
HDDEBUG("unable to get details of the user [%s]", interface->Nickname().String());
}
return result;
}
void
UserUsageConditionsWindow::_NotifyFetchProblem()
{
AppUtils::NotifySimpleError(B_TRANSLATE("Usage conditions download problem"),
B_TRANSLATE(
"An error has arisen downloading the usage "
"conditions. Check the log for details and try again. " ALERT_MSG_LOGS_USER_GUIDE));
}
void
UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
{
if (!Lock()) {
HDERROR("failed to lock window");
} else {
fWorkerThread = thread;
Unlock();
}
}
void
UserUsageConditionsWindow::_DisplayData(const UserDetail& userDetail,
const UserUsageConditions& userUsageConditions)
{
fCopyView->SetText(userUsageConditions.CopyMarkdown());
fAgeNoteStringView->SetText(_MinimumAgeText(userUsageConditions.MinimumAge()));
fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
if (fIntroductionTextView != NULL)
fIntroductionTextView->SetText(_IntroductionTextForMode(fMode, userDetail));
}
const BString
UserUsageConditionsWindow::_VersionText(const BString& code)
{
BString versionText(B_TRANSLATE("Version %Code%"));
versionText.ReplaceAll("%Code%", code);
return versionText;
}
const BString
UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
{
BString ageNoteText;
static BStringFormat formatText(
B_TRANSLATE("Users are required to be "
"{0, plural, one{# year of age} other{# years of age}} or older."));
formatText.Format(ageNoteText, minimumAge);
return ageNoteText;
}
const BString
UserUsageConditionsWindow::_IntroductionTextForMode(UserUsageConditionsSelectionMode mode,
const UserDetail& userDetail)
{
switch (mode) {
case LATEST:
return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
case USER:
{
BString nicknamePresentation = PLACEHOLDER_TEXT;
BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
if (!userDetail.Nickname().IsEmpty())
nicknamePresentation = userDetail.Nickname();
uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
if (timestampAgreed > 0) {
agreedToTimestampPresentation
= LocaleUtils::TimestampToDateTimeString(timestampAgreed);
}
BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
text.ReplaceAll("%Nickname%", nicknamePresentation);
text.ReplaceAll("%AgreedToTimestamp%", agreedToTimestampPresentation);
return text;
}
default:
return "???";
}
}