xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision 9a6a20d4689307142a7ed26a1437ba47e244e73f)
1 /*
2  * Copyright 2019-2021, Andrew Lindesay <apl@lindesay.co.nz>.
3  * All rights reserved. Distributed under the terms of the MIT License.
4  */
5 
6 #include "UserUsageConditionsWindow.h"
7 
8 #include <Button.h>
9 #include <Catalog.h>
10 #include <Font.h>
11 #include <LayoutBuilder.h>
12 #include <ScrollView.h>
13 #include <StringFormat.h>
14 #include <StringView.h>
15 
16 #include "AppUtils.h"
17 #include "BarberPole.h"
18 #include "HaikuDepotConstants.h"
19 #include "LocaleUtils.h"
20 #include "Logger.h"
21 #include "MarkupTextView.h"
22 #include "Model.h"
23 #include "UserUsageConditions.h"
24 #include "ServerHelper.h"
25 #include "TextView.h"
26 #include "WebAppInterface.h"
27 
28 
29 #undef B_TRANSLATION_CONTEXT
30 #define B_TRANSLATION_CONTEXT "UserUsageConditions"
31 
32 #define PLACEHOLDER_TEXT "..."
33 
34 #define INTRODUCTION_TEXT_LATEST "HaikuDepot communicates with a " \
35 	"server component called HaikuDepotServer. These are the latest " \
36 	"usage conditions for use of the HaikuDepotServer service."
37 
38 #define INTRODUCTION_TEXT_USER "HaikuDepot communicates with a " \
39 	"server component called HaikuDepotServer. These are the usage " \
40 	"conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\
41 	"in relation to the use of the HaikuDepotServer service."
42 
43 #define KEY_USER_USAGE_CONDITIONS	"userUsageConditions"
44 #define KEY_USER_DETAIL				"userDetail"
45 
46 /*!	This is the anticipated number of lines of test that appear in the
47 	introduction.
48 */
49 
50 #define LINES_INTRODUCTION_TEXT 2
51 
52 
53 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
54 	UserUsageConditions& userUsageConditions)
55 	:
56 	BWindow(BRect(), B_TRANSLATE("Usage conditions"),
57 			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
58 			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
59 				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
60 	fMode(FIXED),
61 	fModel(model),
62 	fIntroductionTextView(NULL),
63 	fWorkerThread(-1)
64 {
65 	_InitUiControls();
66 
67 	font_height fontHeight;
68 	be_plain_font->GetHeight(&fontHeight);
69 	const float lineHeight = fontHeight.ascent + fontHeight.descent;
70 
71 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
72 		0, false, true, B_PLAIN_BORDER);
73 	scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
74 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
75 		new BMessage(B_QUIT_REQUESTED));
76 
77 	BLayoutBuilder::Group<>(this, B_VERTICAL)
78 		.SetInsets(B_USE_WINDOW_INSETS)
79 		.Add(fVersionStringView, 1)
80 		.Add(scrollView, 97)
81 		.Add(fAgeNoteStringView, 1)
82 		.AddGroup(B_HORIZONTAL, 1)
83 			.AddGlue()
84 			.Add(okButton)
85 			.End()
86 		.End();
87 
88 	GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
89 	ResizeToPreferred();
90 	CenterOnScreen();
91 
92 	UserDetail userDetail;
93 		// invalid user detail
94 	_DisplayData(userDetail, userUsageConditions);
95 }
96 
97 UserUsageConditionsWindow::UserUsageConditionsWindow(
98 	Model& model, UserUsageConditionsSelectionMode mode)
99 	:
100 	BWindow(BRect(), B_TRANSLATE("Usage conditions"),
101 			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
102 			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
103 				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
104 	fMode(mode),
105 	fModel(model),
106 	fWorkerThread(-1)
107 {
108 	_InitUiControls();
109 
110 	font_height fontHeight;
111 	be_plain_font->GetHeight(&fontHeight);
112 	const float lineHeight = fontHeight.ascent + fontHeight.descent;
113 
114 	fWorkerIndicator = new BarberPole("fetch data worker indicator");
115 	BSize workerIndicatorSize;
116 	workerIndicatorSize.SetHeight(lineHeight);
117 	fWorkerIndicator->SetExplicitSize(workerIndicatorSize);
118 
119 	fIntroductionTextView = new TextView("introduction text view");
120 	UserDetail userDetail;
121 	fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail));
122 
123 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
124 		0, false, true, B_PLAIN_BORDER);
125 	scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
126 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
127 		new BMessage(B_QUIT_REQUESTED));
128 
129 	BLayoutBuilder::Group<>(this, B_VERTICAL)
130 		.SetInsets(B_USE_WINDOW_INSETS)
131 		.Add(fIntroductionTextView, 1)
132 		.AddGlue()
133 		.Add(fVersionStringView, 1)
134 		.Add(scrollView, 95)
135 		.Add(fAgeNoteStringView, 1)
136 		.AddGroup(B_HORIZONTAL, 1)
137 			.AddGlue()
138 			.Add(okButton)
139 			.End()
140 		.Add(fWorkerIndicator, 1)
141 		.End();
142 
143 	GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
144 	ResizeToPreferred();
145 	CenterOnScreen();
146 
147 	_FetchData();
148 		// start a new thread to pull down the user usage conditions data.
149 }
150 
151 
152 UserUsageConditionsWindow::~UserUsageConditionsWindow()
153 {
154 }
155 
156 
157 /*! This sets up the UI controls / interface elements that are not specific to
158     a given mode of viewing.
159 */
160 
161 void
162 UserUsageConditionsWindow::_InitUiControls()
163 {
164 	fCopyView = new MarkupTextView("copy view");
165 	fCopyView->SetViewUIColor(B_NO_COLOR);
166 	fCopyView->SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
167 	fCopyView->SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
168 	fCopyView->SetInsets(8.0f);
169 
170 	fAgeNoteStringView = new BStringView("age note string view",
171 		PLACEHOLDER_TEXT);
172 	fAgeNoteStringView->AdoptSystemColors();
173 
174 	BFont versionFont(be_plain_font);
175 	versionFont.SetSize(versionFont.Size() * 0.75f);
176 
177 	fVersionStringView = new BStringView("version string view",
178 		PLACEHOLDER_TEXT);
179 	fVersionStringView->AdoptSystemColors();
180 	fVersionStringView->SetFont(&versionFont);
181 	fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
182 	fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
183 }
184 
185 
186 void
187 UserUsageConditionsWindow::MessageReceived(BMessage* message)
188 {
189 	switch (message->what) {
190 		case MSG_USER_USAGE_CONDITIONS_DATA:
191 		{
192 			BMessage userDetailMessage;
193 			BMessage userUsageConditionsMessage;
194 			message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
195 			message->FindMessage(KEY_USER_USAGE_CONDITIONS,
196 				&userUsageConditionsMessage);
197 			UserDetail userDetail(&userDetailMessage);
198 			UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
199 			_DisplayData(userDetail, userUsageConditions);
200 			fWorkerIndicator->Stop();
201 			break;
202 		}
203 		default:
204 			BWindow::MessageReceived(message);
205 			break;
206 	}
207 }
208 
209 
210 bool
211 UserUsageConditionsWindow::QuitRequested()
212 {
213 	// for now we just don't allow the quit when the background thread
214 	// is processing.  In the future it would be good if the HTTP
215 	// requests were re-organized such that cancellations were easier to
216 	// implement.
217 
218 	if (fWorkerThread == -1)
219 		return true;
220 	HDINFO("unable to quit when the user usage "
221 		"conditions window is still fetching data");
222 	return false;
223 }
224 
225 
226 /*!	This method is called on the main thread in order to initiate the background
227 	processing to obtain the user usage conditions data.  It will take
228 	responsibility for coordinating the creation of the thread and starting the
229 	thread etc...
230 */
231 
232 void
233 UserUsageConditionsWindow::_FetchData()
234 {
235 	if (-1 != fWorkerThread)
236 		debugger("illegal state - attempt to fetch, but fetch in progress");
237 	thread_id thread = spawn_thread(&_FetchDataThreadEntry,
238 		"Fetch usage conditions data", B_NORMAL_PRIORITY, this);
239 	if (thread >= 0) {
240 		fWorkerIndicator->Start();
241 		_SetWorkerThread(thread);
242 		resume_thread(fWorkerThread);
243 	} else {
244 		debugger("unable to start a thread to fetch the user usage "
245 			"conditions.");
246 	}
247 }
248 
249 
250 /*!	This method is called from the thread in order to start the thread; it is
251 	the entry-point for the background processing to obtain the user usage
252 	conditions.
253 */
254 
255 /*static*/ int32
256 UserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
257 {
258 	UserUsageConditionsWindow* win
259 		= reinterpret_cast<UserUsageConditionsWindow*>(data);
260 	win->_FetchDataPerform();
261 	return 0;
262 }
263 
264 
265 /*!	This method will perform the task of obtaining data about the user usage
266 	conditions.
267 */
268 
269 void
270 UserUsageConditionsWindow::_FetchDataPerform()
271 {
272 	UserDetail userDetail;
273 	UserUsageConditions conditions;
274 	WebAppInterface* interface = fModel.GetWebAppInterface();
275 	BString code;
276 	status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code);
277 
278 	if (status == B_OK) {
279 		if (fMode == USER && code.IsEmpty()) {
280 			BString message = B_TRANSLATE(
281 				"The user '%Nickname%' has not agreed to any usage "
282 				"conditions.");
283 			message.ReplaceAll("%Nickname%", userDetail.Nickname());
284 			AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"),
285 				message);
286 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
287 			status = B_BAD_DATA;
288 		}
289 	} else {
290 		_NotifyFetchProblem();
291 		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
292 	}
293 
294 	if (status == B_OK) {
295 		if (interface->RetrieveUserUsageConditions(code, conditions) == B_OK) {
296 			BMessage userUsageConditionsMessage;
297 			BMessage userDetailMessage;
298 			conditions.Archive(&userUsageConditionsMessage, true);
299 			userDetail.Archive(&userDetailMessage, true);
300 			BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
301 			dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS,
302 				&userUsageConditionsMessage);
303 			dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage);
304 			BMessenger(this).SendMessage(&dataMessage);
305 		} else {
306 			_NotifyFetchProblem();
307 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
308 		}
309 	}
310 
311 	_SetWorkerThread(-1);
312 }
313 
314 
315 status_t
316 UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform(
317 	UserDetail& userDetail, BString& code)
318 {
319 	switch (fMode) {
320 		case LATEST:
321 			code.SetTo("");
322 				// no code in order to get the latest
323 			return B_OK;
324 		case USER:
325 			return _FetchUserUsageConditionsCodeForUserPerform(
326 				userDetail, code);
327 		default:
328 			debugger("unhanded mode");
329 			return B_ERROR;
330 	}
331 }
332 
333 
334 status_t
335 UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform(
336 	UserDetail& userDetail, BString& code)
337 {
338 	WebAppInterface* interface = fModel.GetWebAppInterface();
339 
340 	if (interface->Nickname().IsEmpty())
341 		debugger("attempt to get user details for the current user, but"
342 			" there is no current user");
343 
344 	BMessage responseEnvelopeMessage;
345 	status_t result = interface->RetrieveCurrentUserDetail(responseEnvelopeMessage);
346 
347 	if (result == B_OK) {
348 		// could be an error or could be a valid response envelope
349 		// containing data.
350 		switch (WebAppInterface::ErrorCodeFromResponse(responseEnvelopeMessage)) {
351 			case ERROR_CODE_NONE:
352 				result = WebAppInterface::UnpackUserDetail(
353 					responseEnvelopeMessage, userDetail);
354 				break;
355 			default:
356 				ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage);
357 				result = B_ERROR;
358 					// just any old error to stop
359 				break;
360 		}
361 	} else {
362 		HDERROR("an error has arisen communicating with the"
363 			" server to obtain data for a user's user usage conditions"
364 			" [%s]", strerror(result));
365 		ServerHelper::NotifyTransportError(result);
366 	}
367 
368 	if (result == B_OK) {
369 		BString userUsageConditionsCode = userDetail.Agreement().Code();
370 		HDDEBUG("the user [%s] has agreed to uuc [%s]",
371 			interface->Nickname().String(),
372 			userUsageConditionsCode.String());
373 		code.SetTo(userUsageConditionsCode);
374 	} else {
375 		HDDEBUG("unable to get details of the user [%s]",
376 			interface->Nickname().String());
377 	}
378 
379 	return result;
380 }
381 
382 
383 void
384 UserUsageConditionsWindow::_NotifyFetchProblem()
385 {
386 	AppUtils::NotifySimpleError(
387 		B_TRANSLATE("Usage conditions download problem"),
388 		B_TRANSLATE("An error has arisen downloading the usage "
389 			"conditions. Check the log for details and try again. "
390 			ALERT_MSG_LOGS_USER_GUIDE));
391 }
392 
393 
394 void
395 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
396 {
397 	if (!Lock())
398 		HDERROR("failed to lock window");
399 	else {
400 		fWorkerThread = thread;
401 		Unlock();
402 	}
403 }
404 
405 
406 void
407 UserUsageConditionsWindow::_DisplayData(
408 	const UserDetail& userDetail,
409 	const UserUsageConditions& userUsageConditions)
410 {
411 	fCopyView->SetText(userUsageConditions.CopyMarkdown());
412 	fAgeNoteStringView->SetText(_MinimumAgeText(
413 		userUsageConditions.MinimumAge()));
414 	fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
415 	if (fIntroductionTextView != NULL) {
416 		fIntroductionTextView->SetText(
417 			_IntroductionTextForMode(fMode, userDetail));
418 	}
419 }
420 
421 
422 /*static*/ const BString
423 UserUsageConditionsWindow::_VersionText(const BString& code)
424 {
425 	BString versionText(
426 		B_TRANSLATE("Version %Code%"));
427 	versionText.ReplaceAll("%Code%", code);
428 	return versionText;
429 }
430 
431 
432 /*static*/ const BString
433 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
434 {
435 	BString ageNoteText;
436 	static BStringFormat formatText(B_TRANSLATE("Users are required to be "
437 		"{0, plural, one{# year of age} other{# years of age}} or older."));
438 	formatText.Format(ageNoteText, minimumAge);
439 	return ageNoteText;
440 }
441 
442 
443 /*static*/ const BString
444 UserUsageConditionsWindow::_IntroductionTextForMode(
445 	UserUsageConditionsSelectionMode mode,
446 	const UserDetail& userDetail)
447 {
448 	switch (mode) {
449 		case LATEST:
450 			return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
451 		case USER:
452 		{
453 			BString nicknamePresentation = PLACEHOLDER_TEXT;
454 			BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
455 
456 			if (!userDetail.Nickname().IsEmpty())
457 				nicknamePresentation = userDetail.Nickname();
458 
459 			uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
460 
461 			if (timestampAgreed > 0) {
462 				agreedToTimestampPresentation =
463 					LocaleUtils::TimestampToDateTimeString(timestampAgreed);
464 			}
465 
466 			BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
467 			text.ReplaceAll("%Nickname%", nicknamePresentation);
468 			text.ReplaceAll("%AgreedToTimestamp%",
469 				agreedToTimestampPresentation);
470 			return text;
471 		}
472 		default:
473 			return "???";
474 	}
475 }
476