xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision 125b262675217084e0c59014b4a98f724f1c4fb3)
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->SetLowColor(RGB_COLOR_WHITE);
167 	fCopyView->SetInsets(8.0f);
168 
169 	fAgeNoteStringView = new BStringView("age note string view",
170 		PLACEHOLDER_TEXT);
171 	fAgeNoteStringView->AdoptSystemColors();
172 
173 	BFont versionFont(be_plain_font);
174 	versionFont.SetSize(versionFont.Size() * 0.75f);
175 
176 	fVersionStringView = new BStringView("version string view",
177 		PLACEHOLDER_TEXT);
178 	fVersionStringView->AdoptSystemColors();
179 	fVersionStringView->SetFont(&versionFont);
180 	fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
181 	fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
182 }
183 
184 
185 void
186 UserUsageConditionsWindow::MessageReceived(BMessage* message)
187 {
188 	switch (message->what) {
189 		case MSG_USER_USAGE_CONDITIONS_DATA:
190 		{
191 			BMessage userDetailMessage;
192 			BMessage userUsageConditionsMessage;
193 			message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
194 			message->FindMessage(KEY_USER_USAGE_CONDITIONS,
195 				&userUsageConditionsMessage);
196 			UserDetail userDetail(&userDetailMessage);
197 			UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
198 			_DisplayData(userDetail, userUsageConditions);
199 			fWorkerIndicator->Stop();
200 			break;
201 		}
202 		default:
203 			BWindow::MessageReceived(message);
204 			break;
205 	}
206 }
207 
208 
209 bool
210 UserUsageConditionsWindow::QuitRequested()
211 {
212 	// for now we just don't allow the quit when the background thread
213 	// is processing.  In the future it would be good if the HTTP
214 	// requests were re-organized such that cancellations were easier to
215 	// implement.
216 
217 	if (fWorkerThread == -1)
218 		return true;
219 	HDINFO("unable to quit when the user usage "
220 		"conditions window is still fetching data");
221 	return false;
222 }
223 
224 
225 /*!	This method is called on the main thread in order to initiate the background
226 	processing to obtain the user usage conditions data.  It will take
227 	responsibility for coordinating the creation of the thread and starting the
228 	thread etc...
229 */
230 
231 void
232 UserUsageConditionsWindow::_FetchData()
233 {
234 	if (-1 != fWorkerThread)
235 		debugger("illegal state - attempt to fetch, but fetch in progress");
236 	thread_id thread = spawn_thread(&_FetchDataThreadEntry,
237 		"Fetch usage conditions data", B_NORMAL_PRIORITY, this);
238 	if (thread >= 0) {
239 		fWorkerIndicator->Start();
240 		_SetWorkerThread(thread);
241 		resume_thread(fWorkerThread);
242 	} else {
243 		debugger("unable to start a thread to fetch the user usage "
244 			"conditions.");
245 	}
246 }
247 
248 
249 /*!	This method is called from the thread in order to start the thread; it is
250 	the entry-point for the background processing to obtain the user usage
251 	conditions.
252 */
253 
254 /*static*/ int32
255 UserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
256 {
257 	UserUsageConditionsWindow* win
258 		= reinterpret_cast<UserUsageConditionsWindow*>(data);
259 	win->_FetchDataPerform();
260 	return 0;
261 }
262 
263 
264 /*!	This method will perform the task of obtaining data about the user usage
265 	conditions.
266 */
267 
268 void
269 UserUsageConditionsWindow::_FetchDataPerform()
270 {
271 	UserDetail userDetail;
272 	UserUsageConditions conditions;
273 	WebAppInterface interface = fModel.GetWebAppInterface();
274 	BString code;
275 	status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code);
276 
277 	if (status == B_OK) {
278 		if (fMode == USER && code.IsEmpty()) {
279 			BString message = B_TRANSLATE(
280 				"The user '%Nickname%' has not agreed to any usage "
281 				"conditions.");
282 			message.ReplaceAll("%Nickname%", userDetail.Nickname());
283 			AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"),
284 				message);
285 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
286 			status = B_BAD_DATA;
287 		}
288 	} else {
289 		_NotifyFetchProblem();
290 		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
291 	}
292 
293 	if (status == B_OK) {
294 		if (interface.RetrieveUserUsageConditions(code, conditions) == B_OK) {
295 			BMessage userUsageConditionsMessage;
296 			BMessage userDetailMessage;
297 			conditions.Archive(&userUsageConditionsMessage, true);
298 			userDetail.Archive(&userDetailMessage, true);
299 			BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
300 			dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS,
301 				&userUsageConditionsMessage);
302 			dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage);
303 			BMessenger(this).SendMessage(&dataMessage);
304 		} else {
305 			_NotifyFetchProblem();
306 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
307 		}
308 	}
309 
310 	_SetWorkerThread(-1);
311 }
312 
313 
314 status_t
315 UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform(
316 	UserDetail& userDetail, BString& code)
317 {
318 	switch (fMode) {
319 		case LATEST:
320 			code.SetTo("");
321 				// no code in order to get the latest
322 			return B_OK;
323 		case USER:
324 			return _FetchUserUsageConditionsCodeForUserPerform(
325 				userDetail, code);
326 		default:
327 			debugger("unhanded mode");
328 			return B_ERROR;
329 	}
330 }
331 
332 
333 status_t
334 UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform(
335 	UserDetail& userDetail, BString& code)
336 {
337 	WebAppInterface interface = fModel.GetWebAppInterface();
338 
339 	if (interface.Nickname().IsEmpty())
340 		debugger("attempt to get user details for the current user, but"
341 			" there is no current user");
342 
343 	BMessage responseEnvelopeMessage;
344 	status_t result = interface.RetrieveCurrentUserDetail(
345 		responseEnvelopeMessage);
346 
347 	if (result == B_OK) {
348 		// could be an error or could be a valid response envelope
349 		// containing data.
350 		switch (interface.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